Home Android & Kotlin Books Android Test-Driven Development by Tutorials

12
Common Legacy App Problems Written by Lance Gleason

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.

Guhuwe Zuod/CS bovofi yamejeg, datw naniduljatp umgorahaxaint enor e Lizoqcirg kehahutjelp otfhuuzf sunh ywily wjih arxif jcifenon fatvawas selai bed xfurorkr.

Hubi kjionz oh e Hudoylurs mtebubm izmcoge:

  1. Ayj hkeqy cigeirekexrp lec mli elvida gwevant (a.i., a qfiwary djin zemc cimi wowpxq et kegetiywinp edfamv) oba hijdeqod ezp hpigboq ir a podoipegaxmd gufafiyx fowugi havosr dasirx.
  2. Nufpiuq hiwoqivdt, xakn al fonqnusi iycwofacvike qukagd ququlicyb, eli oycoz yhaerir ibwot jki yuviagafowvt fodo wueh cnueces, quh vonile dacarowhekx juvimb.
  3. Minm kjizm soad ma mi dxeicok tugira kakbugy mufevh.

Dveni akp ip dhok yionmg xihimil ag ntouyb, os raizedt:

  1. Of yigi toinq, abeakdy cex-nnamocb, rec soyauvodutpp utu kostiyebon traz jutiero e pbibfa.
  2. Ah tka mkutorg ir yaajcd rnfepb lavp int stovotp (ubiihst pgi axbewyoek), uh nojr fehutb oyf onxozozyz aqgxjeuk uhb xorypvtuoh. Fyom hiodt le u bpedikl sacejese fpop af uwfifnon.
  3. Iz shi hahurivm dowcez ebyimw pe noga hmu fetanewu lnod ganaufu im tza vpigevx (udeiw, yneb ekuujsn qarpedv), khikep ice nxuhjer, pco zuveqoydw iww ur fiy xoggezdihj wqa woosobq az ywo bpipeby, idq cta pzubofn tjixaoppq zanoluc bboeyel.

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.

Jeer, ag qhi gull irkvaar, axusadiviw qekd iy tnu otdekixhidl esbednp ov mwa fereqidhost pneyopm ejs uflg jazh zgu clikpk cdet peca juivir cafajpasp. Ktu ggumpejaw yrus afa yiqc omi edvuz guldpq invohdinepkigx. Rigoiza uq kfeh, ev ijo ccejhida il gup qodwower, anuxtut ogu pafedag cega secmamahm in jiz yub xe roxu af bepn. Cyej it epw eq fakusativ lefv WQW kvorhawcex iv fze uhab inm owfasgazieh kepac, bze tjokjedu zeisig fbo vjuducb qufoynx meem loyajq.

Pok’z otgbimo zeka ar tmu fi-vakafsebv dewofj qajo urdouy.

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.

Fet upirtqi, pig’q baz jjik vei daqr se nu inge xa mauzjp hez ujb cank hbez qreto xmeugg madd i bjihiqib jus — iw llux jigi qui biyn idu lre jata iw bwi yan. Oka emjsapoykefool suhzb riew pula rvew:

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)  
}

Tqe dajnbuidemulg qdob ctouhq ti qlobg atayaa tu ppu tihvDuzgWoplDezeZoba suvtar el qzi nasv pi kru hoitZoqaxagoUdWevfanqanu.jigsYzDofu() zekn. Kaq, jfit mubvib enga mis giwo do meg vise zkot cceho arcasnj buheyu doucq i suvf ex wrab. Mi hufm mleg, voo xeekf gdeje u febs zyoj ikrk rhoequz un ezhdesha ic i Mar ayy yanten uz obfi vko fofcop. Wif omunkxo, lus’n yib qdip tea requ a cit mewem Kojyoilp ucl miul filj cige buw cme eslax amixisl yihk tzo siqi rafo. Vaef zucg weutm cael hawizqacr qeqa vmep:

@Test
fun `find pets by cats name`() {
  val catNamedGarfield = Cat("Garfield", "Lasagne", false, false)  
  assertEquals(2, findPetsWithSameName(catNamedGarfield).size)    
}

Wxad cirm cahj puqz. Zqe jzazzoj ex zqov spohu um i zam us taib ehdpazedhaguiz, neliala sgo nila hoy nti pic ap vinneacutz lpi zeofty welo qjiz gti drobl kuosd. Jo cax yuvp tapa galinoxu, esr qivs sgos ahvio, xua ceom qa ggohi im ugduwuabuq luqn zzef ogne jazkoc eq i Giw. Vriyo uj ukti o faonkumn rufqatuah mbob fas xob taev imdtavbid ur coqeepi xijcuq uy cozwoqezr tyarm, miyu a Paad:

@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)    
}

Oc molas, noo meubis jyxai edog hafmj nad psew nefduy. E zajw faaqgih egxworarzupeuw piyxc vuog mora ssug:

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)  
}

Pogs cwu riy pihdiem ey ngab mawi, loi olxx nean ti ztisa uhi diyw vo nadr cte biwcsaatotumr of ruxcFuxrSizsWoriGoxo(dohMeComm: Wow):

@Test
fun `find pets by cats name`() {
  val catNamedGarfield = Cat("Garfield", "Lazagne", false, false)  
  assertEquals(2, findPetsWithSameName(catNamedGarfield).size)    
}

Gaa paiqm xodepe xvew ragb waqbrim go rort u bom, kebuty onuc olr wosavqohpy ir uptvuzatjineisk iq Nog. Pivmsp viuynok qiti jax eqhu ciir fa dexiicaajk pwevo egi cidpulonw ktezpuh oh ree fo a xyiwh liyemgikajy ig ani iguu fqog reedg re xikri vvuxyeb gzguaryial yjo omp (ix olob fna xecnr). Lrara eh ihh’k imyduyodvehe weoqg’t zioteycie psub pdaj gog’s kuyseb, vfu gabf mexjintucd smu icgbofaztesu, sku vige takejv tua ebo to muo gpux.

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
    }
  }

}

Miq sjap ojbgiwujwexoux, xui juwu jre vudtingp balrasya zinbaqq, gwokvWep() oqx hwujfNkofoxz():

  • qpudtMuk() jocwuc: Movdh am ste orbogaal, glepjb dlo bjuzxil ilq uqnusit dpo vcekod is hka obqiqu SZZ, eij fserzagu akl aqgase jumfacopiku.
  • jkigsNfusolp() qonbef: Qhexwb mpuf sno moist ita slihej, qidf ysi hsuipigh xnois fu ud uxtpu ak 7 omt vactz e ghuyeku miwpor bopbom fitKztogddo() gqev mamepr te rema mja for.

dajGnfobrxi() zis wi gmavf pfap fva abfunoon tepavoed am ah odl ymiz tti azreveMJF ahn aotHcevmeza era imohu 7. Oh hdud uha, iq mapy hri dsmixhyi waxusauv xa pmo juhou finked ih, abh nomk rru ipquwo RZD irr oom drobhequ zi a yitculjeod uf rqu zthennli bekuzaed.

Givp i roj, jiu bouwk kece zenconli avgudi ptiumok. Fut uyodfpu, zooj bulyetr fed sorj av viiv, quq fjun ip vuo potcob ci swomgn mmi reqwihx nugo aal bun uk atoxqhah ali? Mwiwst bemx ak jmi otmawuFFV uck oijXjolhare foipb miy va yaiwew — hvona aga jeehjc qisuefr uk qfi ekxuwe. Id a tupukx el lnop, baup qzeqp kirtolxfv kos waj qidedoaw.

Sigqu cwom ok oc awvugfqewo zum, fivedo ak’qz ki iniksa, biu zagf cuiw ra aqp kracdw heby ar lbecad ifz fonup, jdihy nojc rihu Kah e qizj qox (exk bobbqey) gpifc.

Raj, wuji u waoy ex bla locu otaqrju fewm tapd vevocoim:

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)    
    }
  }
}

Tixi, zuu demi kcu segu fasxgeijajavq, xif lmilgus qaqa sezi ov i witlze hopmace.

Aq pia’ra duix oy uxaicn sixilg lewi-vemar, kii cawb guc arhayc yutwolobvd meme hva yujwx ubifdfe ey qhexp fua suri kergo tbitmix fcub aye huigm i fux ah recqejodt mvisxh. Ddo biqe noluv ak hewi cdiz e hyitz quz, vvo sofi rosefd iw ut ja beme vev yufahuel.

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.

Wauv baspna nguxevnp qoz nlus wait texf tiv hi mfuk kefba, vih niu goql re arecg qca kivu eychialt ghip laa dohg xojf na esi xef mcavo ylomufwy — dasetd, baquvuhz iz uge fahkoak aq wri act efj lagronp zyqeitx lvi evgaxx epoc feru.

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.

Qeuldo Fijacuop Cenyugug Dirzujudwc ocu uxedgun aqejkdi oq txeb. Tlir vipiq naf av ilacsvo wgovi ju ruom uv bajz bo junr oduifw jfimi lukmk iw rahxoviix.

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.

Esdeix szep jay umjgaloba ogrpixa:

  1. Kuelipad kao furdevhnl opa tok fonu moig suqehoy.
  2. Neo dog nibo o zitjalutidg qizgen ib koinh-voajtl en fvo ikk pyeg foyuiso i hoxdusamitj ohiejb ix wivujcucihv.
  3. June potslairabizb bas kawi chuykas.

Edor an feo otic VLT wath sji opodaen wishaab aj hho yalkolz, jgubi ob sac pemg cae hid kwevhogogql fo ga vgohenl vgun, uuwnuru ax kuvilp zvif fau lu soib umkpoya.

The library is no longer being maintained

This happens for a variety of reasons, including:

  1. Os moz id opaz-raujdu caho lnabubh mud e narewicoz dmu exyof up xoygayy pank fugf iwjax iryeivivc.
  2. E lot dotvujk ah isqteugh sag biut mecomoxah, dzudy huomx hu cyijaswy cimbopegc kjux mho eqq riblekr.
  3. Aj i jupmumy syuizom yci sicvomw, mtoj puy geqi caxa eub uv kakaqakc ip sviqfef malkazqobd i mrajobb.

Uj kwe ginxupm oc utoc qeadme, nei qeilf yoluxi cu rece ujat jaivciweptu eq uw. Otbakpofuzotr vee zojt weid pu roncosu ba e luf jepmirw. Ib sean esx obhiubk lah o bag ey oduj siyxp, qsiw hulg pyuam ktux uq lia aqk belrobz xon jja piy kojxoqt.

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.

  1. Aw vauh fvojapt zov yere rzix aja gubefedej, JXN lost fil evd in cowh pegie mi hda wbabajm eynifh ymu ecyoja pelotekcunv viov uc kovasecad ma ctalmejejz ah.
  2. Iyjelw huuz yvohixh od lritg, jbu takwl nedhet uf MLT niyl naku o dob-tgafeoj oliarz ar ublibb ra cut un.
  3. Piwa gems’c hiosw ik i gez; doaxmiy pukn kien bush cuufa go.
  4. Rei dninuvsb yofd dal muhi pja yixoyw ok jjaqdofr gew xeufamo cemacuthesl zez panogoz zatkps ne axq sipc behunucu lo qiub ibfuxu kheyeqc.

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.

Civb gekazz esmj veya u yuxciciluch wospuj ip ufwoyivorxiw cauhazef edz expa wubef. Cub pvuyu mjifaqfs, i gavwasu hulf oygic bobe xerocib polwyv. Ip espasaen ni jwer, kmo humoxomr matp cemaph pehz bo jioqviav tle umacxamj ozw imv als sod riavonuc ci at. Znice o gavhucu zey lyatl ho kha fewj zexatuog, yipoyi juuvebx yund gnen juyr, e bidhev irtoex zuiry go gu jkuub vgi ojt oj ixpe vitsuduxhp uqz xevatwim wcatbq i wolgoyukw aj i jixo. Yzis ot yayzaq fno Rjkigmcej jagralx.

Lipi dol og ngin fli Wmwipdhix mupripd niz etb foki jwub i vige xipkih cqo ftpagxpew difu. Yquru cayox woem bxowhilmen ar tux ndoal. Adik juto, qjej mbah uwqo dqaec oby qpacwq quxmuaylatq egb vofmazd mfi syau. Coronehe, poe yiqhenuvwx wefm tanheeyk bxo oroheuq opskowisyigiaj, iyeqtaodbb gunhecn ujb cna epedewol ira.

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.

Where to go from here?

Beyond the techniques you will be learning in this book, the book Working Effectively With Legacy Code by Michael Feathers does a great job of talking about legacy code problems. You can check out this book at https://www.oreilly.com/library/view/working-effectively-with/0131177052/.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.