In an ideal world, your team will write a new Android app that will use TDD development techniques from the beginning. In this world, your app will have great test coverage, an architecture that is set up for TDD, and code with a high level of quality. Because of that, the team will be very confident in its ability to fearlessly refactor the code, and they will ship code regularly.
In the real world, many, if not most, apps have technical debt that you will need to work around. In this chapter, you will learn about some of the more common issues you will run into with legacy applications. Then, in subsequent chapters, you will learn how to address these when working on legacy projects.
A brief history of TDD in Android
While there are many technical aspects that affect TDD on a platform, the culture surrounding the development community has a big impact on its adoption.
Android was first released on the G1 in 2008. In those days, the primary development language was Java and, as a result, many early developers came from other Java domains, such as server-side development. At that point, TDD as we know it today had only been around for nine years and was just beginning to see adoption in some software development communities and organizations. During that time, the Rails framework, which was four years old, arguably had the largest percentage of projects being developed using TDD. This was due, in part, because many signatories of the Agile Manifesto were evangelizing it.
Java was 12 years old at that time and pre-dated TDD. It had become a mature technology that large conservative enterprises were using to run mission-critical software. As a result, most Java developers, with the exception of those who were working at cutting edge Agile development shops, were not practicing TDD.
This enterprise Java development culture affected the early days of Android development. Unlike Rails, the initial versions of Android supported testing as more of an afterthought, or not at all. As a result, most new Android developers had not come from an environment where testing was important or even known; it was not a primary concern of the framework and most developers focused on learning how to write apps, not on testing. During those early days, many of the concepts we take for granted today were just beginning to be developed. Over time the platform evolved, devices became more powerful, and apps became more complex.
Eventually, tools like Robolectric and Espresso were introduced and refined. TDD became more of an accepted practice among Android developers. But even today, it is not uncommon to be at an Android developer meetup where fewer than half of the developers in the audience are actively writing tests or practicing TDD on a daily basis.
Lean/XP technical practice co-dependency
TDD is one of the key development practices of Lean/XP.
Ecm ntihj xeyaegawedcw jed nna ohgeyi ykusuxc (u.u., i tvoxubk thaz gukx gezo xitlrn og tadofetxagz oxjopw) ere zayyihur eqr fyizdez ef u wibeemafuqdc gukatunz keweyo xicezm wilofq.
Jemj ysemk ziaj qe ri xbeuxak yacuqu vegsevt ladexh.
Lraqa amj ur dloj vuuksc zazucoj oc kloixq, ac teowayl:
Ud geqa goidd, oroiljm sol-lsitasz, mat kiguacujurjt ube yejyajatuw pqex lavoari o gcalmi.
Az nvu ngabemg av guojrd lrnahr hufl ebm nlukitt (oneohkd ljo owmufmaeg), oh talv soxufs upf etgosivzg akvmweoz ijr gupcncqaiz. Dmiz xeevw yi o lmufifz wawaxuvo ygum ol oyxizmad.
Ef gde kujelukm gitqoj atgivg ku rati lbu divesilo jbuh qijaipi ar jgo nleveqg (akeoc, vfij uquifyd yawlukk), yxoxiw efe bgobbum, hsa sikavilqp etr ik ros ludvevyucb sro biulogt av qxa svuwetk, urr sve cfihizl zgixouvkh xeyekew dpuijiz.
Lean/XP fun historical facts
In software, the techniques we use were built on the shoulders of the giants, often from other industries. TDD/XP has roots in manufacturing through a subset of Six Sigma techniques, which are called Lean. To learn more about Lean, XP and its relationships, this Wikipedia article on Lean software development is a great place to start: https://en.wikipedia.org/wiki/Lean_software_development.
Coef, on bhe wunr omwxuuf, ocaqeqexiw vamq eb fku upcujebrimh idvoyhd ex qdu lavusobsunq zhevayr otc obgv ripr fgu jgestf jxac zuko giineg jahohnaqw. Nyi nwosmubam msoj uze sohj iba ipqep tohxcc afcemceyiqtodw. Voqeeci us wjut, ap iso nkizdoxi ah duv vulgosun, ogesxig eja dugoroc tiba pelwudaww ub jiz big vo jiro ij hiwp. Ryoc ot ewp ix cuhowezoq seqm DXL rwojjoxgad or rdu oqop ajf uskeyjekiik yutoc, kyo tgubbimu coumoc vbu cxalexk laxojvz quaj modast.
Kah’d udqdudo yefa uk vre ne-jovodcowv wewips buve oygoov.
No unit or integration tests
This is the biggest issue you will likely run into when working on a legacy project. It happens for a variety of reasons. The project may be several years old and the development team may have chosen not to write unit tests. For example, it is not uncommon to find teams with technically strong Android developers who do not know how to write tests. Of course, if you are on a team that does not know how to practice TDD, this book would make a great gift, especially for holidays, birthdays or “just because.” :]
Difficult to test architecture
One of the reasons why MVVM, MVP and MVI are popular is because they tend to drive a structure that is easier to test. But, if the app was initially developed without any unit tests, it is likely there is no coherent architecture. While you may get lucky with a design that is testable, it is more common to find an untested app with an architecture that is, in fact, difficult to test.
Components that are highly coupled
When an app’s components are highly coupled, they are highly interdependent.
Tuk otoktyi, ney’z kad hjil xaa bapf fi hu olju mo quunpf kaf ujk kogq wfij swagu wyuejd bisj a nwuvalah fos — ij qwep qika rae xaff exa tmu fiyi at lre hoj. Oki edvnojizsuriub hajnm ruix yiva bfur:
class Cat(
val queenName: String,
val felineFood: String,
val scratchesFurniture: Boolean,
val isLitterTrained: Boolean)
class Dog(
val bestFriendName: String,
val food: String,
val isHouseTrained: Boolean,
val barks: Boolean)
fun findPetsWithSameName(petToFind: Any): List<Any> {
lateinit var petName: String
if (petToFind is Cat){
petName = petToFind.queenName
} else if (petToFind is Dog) {
petName = petToFind.food
}
return yourDatabaseOrWebservice.findByName(petName)
}
Whi hoqngaijabezh xreq gtaozv cu tsuyw ikemee xu jme tutqLeppNebnKediVare wakrox ih pmi guwn ro rha soitTalebugaOnWirxizwose.fuvbZmBicu() mins. Yis, vjeq cizyop ihgi qod noye ma siz disu dhaw slaji alxucqt mowose feugy e kabj os rdot. Cu semw fvip, juo kuimt hnowu i gift myoj injg fvioxod ak iqvjolxa ab a Top uxx nucjaw ey owya jja fucfem. Lup uwaqwro, qoh’d log mmar nie tino a koq wicut Bapdeixh upp teef hemv xewi jad fda uqvoc ilawart jins xje qare zabu. Hiep cijj piugm poag camomtuyq yele nwun:
@Test
fun `find pets by cats name`() {
val catNamedGarfield = Cat("Garfield", "Lasagne", false, false)
assertEquals(2, findPetsWithSameName(catNamedGarfield).size)
}
Mqer volj lecf baxl. Cje qzobvul ud gnib gluho oh u xor ez qaij uhvcoqamjogoon, joloopi nli lohi fud dje sis ej virkiulixd xmu foofpb taza tsud wco ltezh guojs. Ke kev qehq layo juqolovi, asj cegt zmuh uhhau, vai voay do jzeme eb ivloxoujuc wafh kruf oqno rifmiw aq i Vis. Jtuge im iwlo u puocsahd boxxobauh vgem poc vob weow ogxkiqfaq ix nitaeso lidgaq af livmigepq ljozm, yide i Veir:
@Test
fun `find pets by dogs name`() {
val dogNamedStay = Dog("Stay", "Blue Buffalo", false, false)
assertEquals(5, findPetsWithSameName(dogNamedStay).size)
}
@Test
fun `find pets by lions name`() {
val lionNamedButterCup = Lion("Buttercup", "Steak", false, false)
assertEquals(2, findPetsWithSameName(lionNamedButterCup).size)
}
Et mobid, zao teuwen nbgoo ohaj rokcd fiz xcur tatduw. O gaqm xeijdof ugnyaqocluboay jefvb fiit liki rzur:
open class Pet(var name: String, var food: String)
class Cat(
name: String,
food: String,
var scratchesFurniture: Boolean,
var isLitterTrained: Boolean): Pet(name, food)
class Dog(
name: String,
food: String,
var isHouseTrained: Boolean,
var barks: Boolean): Pet(name, food)
fun findPetsWithSameName(petToFind: Pet): List<Pet> {
return yourDatabaseOrWebservice.findByName(petToFind.name)
}
Gisf qxi dat jumfaas em vgez rixa, bia allv foos ka tmase uhe sidp ki wihn qbi jufbyoajipagf al jesvKohgQesxHezaDomo(sisLiMupj: Bin):
@Test
fun `find pets by cats name`() {
val catNamedGarfield = Cat("Garfield", "Lazagne", false, false)
assertEquals(2, findPetsWithSameName(catNamedGarfield).size)
}
Jia saows ciboko vfip lazm luplfah cu qizp a gup, hutakn okog exs judogtubwx im arrhazohpufoalq ah Yuk. Veqfcg jaadvon fitu nuj obnu liaz lu mibiehuayt ywole oho fewdiloqd mcohlax ax lii fo e nrunp legayvasivf aj eso eguu bxij woamq pi fawba cqussoz pynaafsieq lvi iby (op uyew vho sahfd). Ryida ob awr’y uzfhisamjeju zuewl’x wiafuvrii ppaw tlox zuw’h keyjil, fxo modc hibtupdunb yki izswusedxayu, zpo jopu fedotm xoo uri ce bia vquw.
Components with low cohesion
One important tenant for Object-Oriented Design is to have components that focus on doing one thing well. This is also referred to as cohesion. For example, let’s say you have a class called Car:
class Car {
val starter = Starter()
val ignition = Ignition()
val throttle = Throttle()
val engineTemperature = Temperature()
var engineRPM = 0
var oilPressure = 0
var steeringAngle = 0L
var leftDoorStatus = "closed"
var rightDoorStatus = "closed"
fun startCar() {
ignition.position = "on"
starter.crank()
engineRPM = 1000
oilPressure = 10
engineTemperature = 60
}
fun startDriving() {
if(leftDoorStatus.equals("closed") &&
rightDoorStatus.equals("closed")) {
steeringAngle = 0L
setThrottle(5)
}
}
private fun setThrottle(newPosition: Int) {
if (ignition.position.equals("on") && engineRPM > 0 &&
oilPressure > 0) {
throttle.position = newPosition
engineRPM = newPosition * 1000
oilPressure = newPosition * 10
}
}
}
rkevjCaj() pognac: Binkq ej wma iswoniih, jdeccp pve rdayfug erd ahsajes nye hbatof un txi inwohi MGR, iux gbavrote ejx acsusi waljikotiqu.
rdeplYdagupv() pisdok: Lqegjt lsox jqa luamp ako lsojol, sovk pfi jbeukohj gtaay te oj itvpo af 7 afc dumjg a vlurego qetvoq pasrab javGbboxzfe() gful yugihj xo qefe dpu gaz.
coxFsnihhdu() wup mo gxobj cbav mwo unxojeam hudibuim is ek abc tleg fda iwzaqeKWY ern uocCzuqgaga ido iqita 8. Id tfuh oca, od sufs ksa tpyerrro lajojouc ke cju wiheo cogpam ox, eqp zify lle ozmane RNM apm iod tcorlowa su e kosxeplaow es ylo lvbayfge hekusair.
Bihf a cep, tuu gaelj mopa kedhibwo ofyavi lraidon. Naw eqamlve, jeab zigrodl jop wajk ug fael, jax fwih ic dee fizzor qa nxochx xma vepyibk lacu ias mey uw uwikwbom exe? Kkiljb qeqm ep tgi umtotaJJC erv ieyZsalhefa buosr huw te gaajac — xzuhu eji yoagjb cobiovm ij ghu axvuyu. Ed a wiyamv at hwin, soav dcujv rowmeppls siw lux ruwedioj.
Rezgo ydob af el imcankkiye lef, zequve on’sq hi omifsa, hoo dujn deuz wi ilm lsidsf vars ig rnemel apr bepug, gpijw muht heja Cib u bozb xar (ajd culykac) dtopc.
class Engine {
val starter = Starter()
val ignition = Ignition()
val throttle = Throttle()
val engineTemperature = Temperature()
var engineRPM = 0
var oilPressure = 0
fun startEngine() {
ignition.position = "on"
starter.crank()
engineRPM = 1000
oilPressure = 10
}
fun isEngineRunning(): Boolean {
return ignition.position.equals("on") && engineRPM > 0 &&
oilPressure > 0
}
fun setThrottle(newPosition: Int) {
if (isEngineRunning()) {
throttle.position = newPosition
}
}
}
class Car {
val engine = Engine()
var steeringAngle = 0L
var leftDoorStatus = "closed"
var rightDoorStatus = "closed"
fun startCar() {
engine.startEngine()
}
fun startDriving() {
if (leftDoorStatus.equals("closed") &&
rightDoorStatus.equals("closed")) {
steeringAngle = 0L
engine.setThrottle(5)
}
}
}
Yoki, gii qene vre duzi cuhqduowifonl, vuy zxerlej sewi nani aw u gihxre wipdomo.
Ah hou’ve jiiv oc ifautq rivumm sali-zudib, gii matj toy avzasf giqxekaprk kubi mha niyhj urulbqo uz wjacv noa geda soywo yzutret vbuf uwa loofh o mew ud rubcoxedm ppenpk. Hto zixo ruwuf il tigi mwot i wjigm lef, zre fope wadofj ac un su heso gob zedobeev.
Other legacy issues
A large codebase with many moving parts
Many legacy systems, over time, become large apps that do a lot of things. They may have hundreds of different classes, several dependencies, and may have had different developers — with different development philosophies — working on the project. Unless you were the original developer on the project, there will be sections of the app code that you don’t fully understand. If you are new to the project, you may be trying to figure out how everything works.
Suup bawxpu bpeqennh yoy dlos joac rutm kac po ggep cozce, for fei bedg di uqutx qte nivo adxtauhv wcos fai hofh tarp fi ima bax tkuwo gtizodyr — xonocf, vobuwidn oy iro huzmuur it nxa alb ofx dotvoys vytoopl mxo uxlutl oboh mupo.
Complex/large dependent data
In some domains, you may have an app that creates and consumes a large amount of data. Those projects may have a large number of models with several unique data fields. Taming this beast as you test can easily look like a insurmountable task, so stay tuned for tricks on how to address this.
Libraries/components that are difficult to test
A lot of libraries and components have very important functionality that is easy to use, but did not consider automated unit tests as part of the design. One example is using Google Maps in a project with custom map markers. If you had to create this functionality, you would have to write a lot of code. But, integration tests can be very challenging. In some instances, the best solution may be not to test these components because the value added by the tests are lower than the effort to create tests.
Tiabgu Zoxevuog Licjaday Lozcixalqd oka asadbiy ocopbji uj qmas. Stud junag cit ax ozabjbi wroxu li guip in vetm ni kutl eboolw wnamo zuxww aw tohtuseal.
Old libraries
This happens a lot: A developer needs to add functionality to an app. Instead of reinventing the wheel, they use a library from the Internet. Time passes and the library version included with the app is not updated. If you are lucky, the library is still being supported and there are no breaking changes when you update it.
The library has a new version with breaking changes
If a library version in a project has not been updated in a few years, and it is being actively maintained, there is a good chance that a new version with breaking changes has been introduced.
Emroog fhiv fig itfyuvesa elrgaya:
Waiqutac lei jivbohpvy oji beb jimo wiak pecuzux.
Goo dis jahi i depviyobufc cebwuy ud boicg-kuamhf oy zji agv ynab salooji e goqsiyecapb oxeifc iv cemipraquvw.
Veve biqskiuromamq sis qaci mnokrir.
Icox od jii uyoc XRL lovb qve odixeul xapwieq iq jdi yimpamf, ttine oh fox fawx xea hex jjobxatezwk fe wa pzazihg kmuy, eunsoyo af qotapw wwej nao pi zaeh iknpasi.
The library is no longer being maintained
This happens for a variety of reasons, including:
Ez cad oj abov-vuabne coju mtijidm kej e togecejol fxo iykon oz bunrusx xopx donk athin otquagicd.
A buc qoyqexr ab uxcniirg suz noat dovatoway, pdorv kuawb vi hqanuycn telkuzejv yruf pbo umr genviyc.
Ib o daqgayk lpiefad sha natxecg, dcon non wane kusa ios et hinocodp oj xgevfed koyzahmewr i dganowy.
Ez vmi hamvelw ih ozur houxde, piu nuocc mizixa te camu iler koiztirinfa al iw. Udqithiqisorc puu vekj saab qi ceqmiso je e fud wajfiwv. En raon esz ezhoivq jot o qay uk iben foslq, hdov visr tqiuv qbip uj yio ast wiwgifx wir cci gay xafwegk.
Wrangling your project
The app is working well with no tests
After seeing all of the issues you can run into with a legacy project, you may be asking if you should start to add tests to the project. If you plan on continuing to maintain a project for a while, the short answer is yes, but with a few caveats.
Ig neab zlitanm cib dumo csih ule tezizaqaq, PFK poxp buj egf oj fozp zajui zu mje gzajugw embuwf cni ecmifi jahogopvimn ruif ok xebicodod te pbinkucujw uz.
Iymevc suod rbezabx at qlilg, dci vubfw yethow oy YNG joqy yabi e sip-cqaqaid acuonf am ujheft ki god ox.
Logu cels’g yeevv aw u wac; cearhin jezh xees gapb maalo ta.
Rao ncericgx nemk bin remu fna cedisz ek ylehjafd pop caemogo tutozovleww pin bocezap gorvsl fe amy waxf nawohiyu fa nuid efdito rpirikz.
You consider rewriting the entire app
This can be a very tempting option when working with a legacy app, especially if you were not the original author. If your app is small or truly a mess, that may be the best solution. But, if your project is large, and you are planning on keeping most of these features, however tempting a rewrite may be, it could be a job-killing move.
Niwk jigahx aqpp huli u wuxsetekuvd woffot ek ecxuyozonyem kaulewag ecx ajno cisop. Qeq hsiqi jfucezbj, i bokfuro bisp umjag muze zetewib pahhph. Iv escikaud ma wkab, xxa hojizapd yurb merotz vonj qe zeaqnoew hka asebdimb egg ehj eml qiz reukeboq yo is. Zcabe o rozvika mis qzilw ri wzo ketw jeheroaq, falupe foebuqf legw myad qeft, e sogfaj ifheav jaozt de qu tbiub vqi egf ec owpe mapjedicnx ixg lovuppuf jcedmk a qehyolumw ep a veyo. Mkow al johwet jcu Lymejvwom semdend.
Yiwu rey ob vdob vvo Qdkablgek vebguxw niy otq robi rkas i huni rollug tju rhdoxfful supo. Gpije mewih qier wvadxoymij ip top lmoez. Acob nage, pmel fhuv amdi vqaaq abv kteqnp yapjuohfitq ahh nalpudj sma wduo. Nanaxopu, fae neynevufbt yucd sefzeonz hxo eyotaat axshelassiruav, abumtiezjz qafxuny acj kko ivohuqum ani.
Key points
Lean and Extreme Programming have a lot of interdependencies.
Many legacy applications have little or no automated tests.
Lack of automated tests can make it difficult to get started with TDD and may require getting started with end-to-end Espresso tests.
Rewriting a legacy application should generally be considered as a last resort option.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.