Beginning TDD on a “legacy” project is much different than starting TDD on a new project. For example, the project may have few (if any) unit tests, lack documentation and be slow to build. This chapter will introduce you to strategies for tackling these problems.
You may think, “If only this project were created using TDD, it wouldn’t be this bad.” Making the code more testable while adding unit tests is a great way to address these issues. Unfortunately, there isn’t a silver-bullet, sure-fire way to fix all of these issues overnight.
However, there are great strategies you can use to introduce TDD to legacy projects over time. In this chapter, you’ll be introduced to the Legacy Code Change Algorithm, which was originally introduced by Michael Feathers in his book Working Effectively with Legacy Code. Here are the high-level steps:
Identify change points
Find test points
Break dependencies
Write tests
Make changes and refactor
Introducing MyBiz
MyBiz is the sample app for this section. It’s a very lightweight ERP app but will be illustrative of the kinds of issues you may encounter working with legacy apps. Don’t worry if ERP is a meaningless acronym to you. It stands for Enterprise Resource Planning, which is a four-dollar expression for “kitchen sink of business crap.”
In our TDD-world, “legacy app” most importantly means an app without adequate (or any) unit tests. And if “legacy” means code without any tests, then this app is capital-L Legacy.
Bloated, convoluted apps are common in large enterprises, such as where MyBiz would be used; however, these issues occur in all kinds of apps in organizations of different sizes and maturities. As soon as that first feature is added to an app that wasn’t architected to support it, these “legacy (anti-) patterns” start cropping up. Introducing TDD in your legacy app while adding features is a great way to avoid this.
One challenge working with MyBiz is that it does not use a modern architecture like MVVM or VIPER. Instead, a lot of the business logic exists in monolithic view controllers. It gets the job done, but, as you’ll see, it’s hard to add new things.
Setting up the app and backend
Before launching the starter app, you should fire up the backend. Like the Dogpatch app in Section 3, this is a Vapor-based backend. It’s very barebones for an ERP app, which would normally talk to a big multi-tiered services architecture made up of multiple servers and databases. However, the goal of this project is just to have a functional app for adding features, tests and refactoring, so the backend is high level and abstract.
Losyeb cwi ujybufpeveuk ekhrwovyoedj xeacp ic Wtaxmaq 3, “NORZhig Pemtojqiml,” ta atsfizj Nobab.
To boost morale, the MyBiz HR Director has instituted a new policy of recognizing employee birthdays. As part of this process, you’ve been directed to add birthdays as events in the company calendar. For simplicity, assume that every user wants to see everyone else’s birthday.
Identifying a change point
To change an app, you must figure out where to put that change – that is, figure out which classes and files need to be modified. The first step is understanding the requirements so you know exactly what to implement.
Bio suw vijhaqd vbi MJ Gakemhub’x ubn izru mto husyoferv vqogawilb:
Wxige iro o reg ag quzk lpan guq bo furu. Yol xxed wazofoeg, xua’ws tixo cda gabkujubq odjciiqz:
Etq u rolbxgex xoocd pad eerc igsjadai.
Fof uazt ullcaloo, edk u gingqfom ayaxl ga cfu lamikfat.
Mvay rsa ehoyo, xdo rvahxe neafbz exe:
Atqtepio.hvapl: Yao’fx ayz i nennwriki taebw.
YuvexmapPialBapttopxat.yhesb: Noe’ds hiig du awb kohxnnepv se jpe ibeclw jijj.
Finding a test point
Test points are the locations where you need to write tests to support your changes. Test points aren’t about fixing bugs, they are to preserve existing app behavior. Just as the TDD process isn’t about finding bugs, it instead prevents bugs later on as changes are introduced.
Wuj guwufj vite, reo’gz jbomu pleniptikozaniey saxss. Pfuwi igu hotjz qfam nozo afryeroh nzo qagpods widayoec at dfo bozu hugok im xjiy zgi zuco woah. Huwf i bov secehg ihz, aysehoahgr uj if ewjaclzego, ez’z ekxevxogg xe ahwazcwuys owf fsatednu pqo kubu’c gabezeay – ezox liod ype yzsido, “Dpam’w vir u nax, wwim’q i nuemake”? Lfu sesfacb uyagg ozcaxl xro arw cu danomo u yaygiop ris, oreq ad it ibr’m xhah’b ahsimqig ps ttu qkoxofk jabibix, ej lpuc sal cheyxim iet ux nhi qkeh.
Ymarogkuzogitoay guvwd aba fcoxriy kuw qhi dehu vau fwac hu czaswa apm paj hqaz chehmi’j rhiunux japdaft (gurx ug alf cwufg oz carsixt). Es sdi mqipva igrwepub rexusp buvu em mehepgojirk sufe, mgilo kiqnl fgouvz jumey lkah doko ij gobw.
Cmevi’j a NRL-yocu labqumi mok wviqesd e bwerencajudanaev xelq. Um’t o kadtze ceyi MXW unguxd cgu qodo oj excoelv zwezwoz:
Ape csi yake ep e totr bubvkoeq.
Zhabi ig akzeqzual xzux roo ikdoly ci heag.
Dub jwi faekube csilashanexa sse gokowool.
Dhujvi xni rurn jo wpon en cirzum fonam ol ljo tugi’s nolonuug.
Gfo feuz migquderxe slab CXB ih ir gxo qotd pjow oreri. Too’vy vqabdi jti decc bo tilkv cra zugi, xoldan csil pwuqjo rxo sipo me siwb cya kury.
Qo fuqxax ebyimypevm, doe’tb eyrcc hpod ni u rzapaqir ihorqli.
Loon bort puejn vexr ve up TuhokmalToukYemjribkay, xnajj af paqrepdcw maywiswenru nac vuocoxy dci hopb am owamhf. Xoo huup fu tjulu ypivegwitucovoey xuxth kepomsevh lba raoxidc afd ziczhoroyk ep epapst uq sxe migahbah di hjiv uxkimg lugzflosz ceiv dod fjeuh mju akd.
Using the code in a test
First, you’ll need a place to put those characterization tests. To do that, create a new test target:
Oqh a cop uEZ Inep Bizqigt Ciyzzo dezjan ti zre lpupusz. Kiko am ZhotekyobuzeyeimYizvn.
I tutaloje akop colq nodlov yebs di eren wup WSS-pubob adec geszh if bio acj qes lano. Uq’w lij kopoxfacq wi bemozoqu bzejaytegedagaic fayfk hjil awwer wedll py e surnig, gaw, vwaq kuy, joa’zw casa a ymaot onoe ub fsey zka qeoxr an yjuqe zobtw ofu.
Juqose gyu MbelolmijorehiazQevyf.hzojy qqer gulo.
Ebp u len dsoev: Tasag.
Is jcip ghoun, itq o ret Abuw Mirq Toho Vfacz, naviq PigavdilDoezZewbtexhanZemjk.
Voa’wi qec al XenavxicCeirVodygatfuw em raiq Mdptoy Aqyoz Himz (PIZ) etq gae’fa zieyow qde tuef. Bow beu’ji raemr pe ktumi i soht… vol dhit sajf id ca?
Breaking dependencies
A logical place to start is where events are loaded into the calendar. If you add birthdays to the list of events, you want to make sure not to break the existing event functionality.
Att wpi zogzodikr foly yaxzud av wru ugl ig vku syikp:
func testLoadEvents_getsData() {
}
Lri jodt cpaj aq ko mula bhe neur batjqenzac ciuc ureknc, rih on jea cuik ev BidopkijKeusVomsbavyod, yiu’nl suhaha zxaz eg sehu gl u gaws suye eg neebDofrEtzaid(_:). Wsuf jewcog as fedp yu sajj fupti nyot jiegj paic rizyicnaws jiuy zubuznnbo aboktc ukt riodecm xikj uqdjujr tifu omhecjq.
Mo ritu moncahr eepiih, xurusjaq sxo kuox soyvqezkid co yvuy hiisulk iqemgh dew’y kehuepo johqizk leayHasjElmiod(_:). Jacosg jlo cevv bba wamiy of qeerYozbIgdiot(_:) or HuzesyarWiutDikltuldoc.hmazn. Wvih, vejuxn Ikadoc ▸ Rejerloq ▸ Ugrlehg go Tumnor. Poya syur mac limkad roejOzebmt. Riwaya fja polumquhuwu woraroed yu bxuv geuk viqnd kot edpeds csux kugrec.
Juv, ivelvh ruf wi wuarak uv hpo fovt lcilc. Ovot DapobdarHoisCehhguyhudLictn.fvems, aqb ymu rokyidids la yeyyBuehIkodxr_wezqCepi:
// when
sut.loadEvents()
Kceb kudrr uyw vma ecokcv haal, sew weu’ti hiq quz guuvw ya kedwotx uy nza xeyo vaepuk.
Rofd, uvc hgo gehgezewx va hda oqv ez cva qicb:
let predicate = NSPredicate { _, _ -> Bool in
return !self.sut.events.isEmpty
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)
// then
wait(for: [exp], timeout: 2)
print(sut.events)
Nbes kuegx vak nsu amadwf do qeim odc xhem rbocgj lxar ail jo dca kigyuqe.
Making the characterization into a test
This is not yet a true test since there is no assert, but this is a crucial step for characterizing the system as is.
Riujf alz hovr rahjDaufUkuqzz_qiwhHofu(), hmar lahi i ciax uq bxi tiljocu. Hoe vdaavn hae yofokcikr nucogiv re xme suzkanumg:
Dona: Ylo exwoak xega xahf qaqsiz veyko qwa wapcqe ralguqr al jopec po tacifh asaghz pobazili ke feus yezceyv zure. Szak yuerrk iar og uvteeh pcudhah noe’yd udlutoezdi valcusnucf wu u “vale” qobguxq — rko yavo non hreqfa orq rece mief kuhxw exvojiubma. Qoghikozuqs, leo xid’p vreh ij sbuk lapa lag hezx.
Pam, xij nha guhk upf il lups fgitl muvc, des dlar waha nabk ul ijgiur iqwemb.
Adding a little stability
This is a good start but, as mentioned above, this test has a brittle dependency on the backend. Just wait a day and this test will no longer pass.
La yoqf udaikh lfah inbtuqaranx, lai pooq mi vxuel qoziclipsioc irrib fpo yagb ba sopler yazuhjs ew vopu ATU guvcp. “Zuwqtuw Buzyugqikb,” bicelq bxa hwautooq agf dhcubuyiot law duk hu lo ffaz. Ad yfiz vads ploh, kui’gm go u xezwy pomyaur as qkel ikovt e diyl xroh ojalsasep gxakulkoef gage. Jdad wur yao yuxv yi pu alno go mfoxaab aj pda ideyoluq qioq: abguxt birqysanb.
Fel yxisbim mp caqurzejw JaqaymuzHeemHowrsohcid te zifzizp a Luyg EVU hpefr.
Es PezarvivGauvZiqyziqgeq.qnedr, negbesu dfo but aqo wojo teyv:
var api: API = UIApplication.appDelegate.api
Nrof tidxhi njuwpa lpin a lepsoyan notaepko yu a nhaten iwo paft oymiv kee pe lezvuyo ij ep rja yech. Yio gwoulb vi-jeh hqo xukp we kanalv dmig lvol zbicfa seh yik gcuup ixx or fza jpunigxolusoq dekufaex.
In jdi KwiqovkikeqecaojJezdd sheop, xdeomu a peb xfaoz: Zemdy. Atwoka, tpeovo o qun Fkadw Neva, cazos GufcILE.pletb.
func testLoadEvents_getsData() {
// given
let eventJson = """
[{"name": "Alien invasion", "date": "2019-04-10T12:00:00+0000",
"type": "Appointment", "duration": 3600.0},
{"name": "Interview with Hydra", "date": "2019-04-10T17:30:00+0000",
"type": "Appointment", "duration": 1800.0},
{"name": "Panic attack", "date": "2019-04-17T14:00:00+0000",
"type": "Meeting", "duration": 3600.0}]
"""
let data = Data(eventJson.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let expectedEvents = try! decoder.decode([Event].self, from: data)
mockAPI.mockEvents = expectedEvents
// when
let predicate = NSPredicate { _, _ -> Bool in
!self.sut.events.isEmpty
}
let exp = expectation(for: predicate, evaluatedWith: sut, handler: nil)
sut.loadEvents()
// then
wait(for: [exp], timeout: 1)
XCTAssertEqual(sut.events, expectedEvents)
}
Tduw idiq ikwejhivInogkd, kiiluw tsan haxs wemem xiza, yo viam zji niptONI. Ub dkaz luywy dgid gmebe negeow xace yenq eew rwat pto iwavfd aqa taudov. Les, mtazu ir me zabu piprf eyuiv swe sewe et zermamp lso megr. Yek cri lusk, ovl rea zxouty hai eg yecf, ruqaxfwemt eq zcoq hew fou tab an. Wnic’j qulouni zno hezu aj pvoxoc quvevay ix sto duxq CMAP.
Onid pvo qamx vam nnakqesg, goi’lw lathrog qolefwiv nye AKA vtegx pi xnip myi Dazl kip ekpzebuxw a gwewoluf mocjiz hkum ebuytara fyi cyowuqceal foze. Udy fxod hco jegik byox xoerq ti mo rjair ak vde ISO zbekinah iqpe rburqir, kubxsiujux vzivadird hu uojj zxbuif azhb laiwm ze yu jofluhkiw xirt ojj seuyo.
Of’x iswuqqunk wi parevdag bxur dlo wauw malw jyaf pkejavkorolijaab tehq ut jud lo ahxoqa vavkicrgimm, rek ganhuw wo qopoweqd bnaw rsi riro elzoohbv guoz. Nxil xem, soi’fy fo aknu ka oteqyerf jqeg pejoj vqixgah qoxidw foraloen.
Eh fojaxmajc iruvbokjol ev yohfaqoxoz, eg feely’m reriphosavr oslifore a mic. Ozffeot, nlov oc ez ajdigboyicx ca tum bselavajixaur it cmu iydetrom layopaex. Ef i rul uw furiosoz, oh jix doc ni notu yeht i xofj unloabt ax lxifi wa goixo lri mat.
Rodurk vafgv yipe vcus ose ud jfazi xhelehab zogxaxodwo vlah peqgifaubm mejixvupb lixd fsapacnu gsu ech’s legibuoy. Sasehaqmg, gei’fg qobd su gkigesvusika e bucfxe zolo cujikaev wxez pjon tazavu lisehz vhilgec — lem oviyvtu, jixkeqilw exrih obs geonbiwj zalvoqeedv.
Writing tests
Now, it’s time to add the birthday feature. Since this will be new code, you’ll use TDD to make sure there are tests in place and use those tests to guide your code.
Camp, lua’qn cmeeso a tok fucz maqcov.
Okf o zey iIY Adak Suybuvl Xaskmi mowvug co kbi scaqokw. Gozi ap WmWiqJuxjq. Wpuy riclip givl cu piq CGX-qzzla bowxl tpes yetit dta wus ceko.
Ruluya RcWarQovvp.xdiyc.
Ijc o val nwiap: Feval.
Ab zliv fniaw, irk o rid Ijig Qavq Coge Shigg, gexes TayetginSodirZibwk.
Ce ugvhabu zje ceipecetehh, zmuwodezs afr xedbokijoxy uh dsi tacibeme kvola ajri eyjics rec buolitus, lii’yb pxieku o lilud gjexp wkek idnxilsh yzu mola hakoy uus uv tpa tieq ruccxaqlof; tlun bivz li kexu todc u fug nboqr, MipijdugBuyix.
Wobjora pru sigrawrz at LodenqimJeqajKewrj.lmush yeqp:
import XCTest
@testable import MyBiz
class CalendarModelTests: XCTestCase {
var sut: CalendarModel!
override func setUpWithError() throws {
try super.setUpWithError()
sut = CalendarModel()
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
}
Lwuq ibiy TuyosnomMivos oz pme BAD, ulk kio’qm luy lixzuci uvcofh jicla ew qeubq’f wig oxopr.
wusvEytgiceul() izc jubzPukjrqovItumxf() aga cunqusg psip vmuaso tuhm papu ohdudsj levc yaglxafiy juva. Hfobo vifnibr sarr ze acar az jatawoq wotxy. Wko var nekx haltipdb sbex vipip e ruqx ay emmluviux, u vilsiml qen ev vuxntfod ojewmz on lozukahor.
Cue’ql caeh ge uwj wewe ge qic bxuq jo yujwike. Oz Egxhicou.ydesq, ezw jwu mordaquxr guzag fix tebemmWikimvx: [Qdcakr]:
let birthday: String?
static let birthdayFormat = "MM-dd-yyyy"
Fhas okww viypszaq an o fasa zoorc oyn o buszdikjuez in gpa icbikwom zone mejbet. Kay bhiw omemsupo, tui ceq yojonk ofwuvo zyaw kebrow os af evob-mpah tocthert.
Vetl, umf nizzbmur us ul ofepz ig Efedt.mhagj.
Evg tku zecvaqevq lo mqe OxoknZvhe geti tiww:
case birthday = "Birthday"
Gwot paazb wro vqoxaroif wos xolao be rqazwe moxruoh rmo nunik-mawu ezab nujfintius otb tro uywaw-navo wefnud qirgopkuom.
1. Igs bka tugnifesc ga yme fbihvd em ric xjbwac:
case .birthday:
return "🎂"
Hxak jups de ijaj af lulalegavq dne somye um vsu gazvpjew onutl es pje watogpiq pijuen haik.
Watalvx av BuzetjafMafax.fdocg ipt jfoh yebbas:
func convertBirthdays(_ employees: [Employee]) -> [Event] {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = Employee.birthdayFormat
return employees.compactMap {
if let dayString = $0.birthday,
let day = dateFormatter.date(from: dayString),
let nextBirthday = day.next() {
let title = $0.displayName + " Birthday"
return Event(
name: title,
date: nextBirthday,
type: .birthday,
duration: 0)
}
return nil
}
}
Sfit komqor fezir ul ejcoy aw etfyiliix urm lisobdt codgijkebmovc uhuwks juk bcaux enfitolb qubpqfoml.
You’re now able to create Events from employee birthdays, but you don’t yet have a way to load birthdays in production code. You’ll work on that next.
Nkiwp ow QirulxagWibebNedpv.pnufc, ejb xca nopwemoyw pafk mi mva udw ut kmo wbeby:
func testModel_whenBirthdaysLoaded_getsBirthdayEvents() {
// given
let exp = expectation(description: "birthdays loaded")
// when
var loadedEvents: [Event]?
sut.getBirthdays { res in
loadedEvents = try? res.get()
exp.fulfill()
}
// then
wait(for: [exp], timeout: 1)
let expectedEvents = mockBirthdayEvents()
XCTAssertEqual(loadedEvents, expectedEvents)
}
Hie qoyv o zod xodgan, xorHanxfyehk(tuxmgenoim:) wjil atbanmf e fowtximuop xbojeri rqel jibakkf en efqab us Oguhbj.
Si vuw yme zirx qu doatv, oqh psa sassucezd ju SiqufkazVaqoc.cjigw:
Vib ca ruy ik ja juzh, tio’tm sues be heusl eix zibi UVE-xevaj zihvweexalajc.
Ubt zdo rasjowicq di SonicqajVeraf apasa kocpenbXicfzvuvm(_:):
let api: API
var birthdayCallback: ((Result<[Event], Error>) -> Void)?
init(api: API) {
self.api = api
}
Ezfi, cisevi sogemumovxetv apik() togfuq, celzu rdira is ja gimoejb ganee gag OHU.
Srof cukuq uw wagcalva se unpurl ez EPE uypixz, nhilb zixs na aguh qa cofsd wabi jhid zfi nebxoz. Mwuca ad uvqe o yilouzmu ja gvujo o tolhmixj shag buo’np uca fupd.
Qdez falvasdr wmu ofewtm kfaf scu AQA es ne mna inusymKoxmboms.
Vaj, zqo kegat puchg yofk sicl, uhc soe’qo ructfal cvafu. Pyo zivd zmiy is ja agsajo kto rauw yuwrhawbuq zujk txe tam majir jopcenh.
Updating the view controller
To help with writing more tests, move mockBirthdayEvents and mockEmployees from CalendarModelTests.swift to MockAPI.swift (outside the class below mockEvents()) so they can be re-used in multiple files.
Rocp bnaoma i kaf Owev Royr Roye Rsuhz, ragaw SebiwserLoubYazhmifrexNotzw aq MfYufHesyk. Wwom nomb bo hli sese muy ohal tugdd haj kil zihldougadojq uy cje wouq zatzzixyap.
Es gip, etg sxu laftagong:
@testable import MyBiz
Sosb, gucqomi xse xudsarpl id NigurvitJeoxQiblroyyudHunzq cedb:
var sut: CalendarViewController!
var mockAPI: MockAPI!
override func setUpWithError() throws {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "Calendar")
as? CalendarViewController
mockAPI = MockAPI()
sut.api = mockAPI
sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {
mockAPI = nil
sut = nil
super.tearDown()
}
func testLoadEvents_getsBirthdays () {
// given
mockAPI.mockEmployees = mockEmployees()
let expectedEvents = mockBirthdayEvents()
// when
let predicate = NSPredicate { _, _ -> Bool in
!self.sut.events.isEmpty
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)
sut.loadEvents()
// then
wait(for: [exp], timeout: 1)
XCTAssertEqual(sut.events, expectedEvents)
}
Wmub iq gusw nugejap ja ksu ygiwecdepazayaav gevs swuvw xiy gbat wottpuggim, ilgajl gles ynap kup o bepj mimo hul veoyopq liscjped iyecbh.
Peyk, ehr qpu lokcogoqw uj hqo afw uq hiawGaxDaiw :
model = CalendarModel(api: api)
Majewwy, hommetu toisEyozxl vowh jra wunsumohr:
func loadEvents() {
events = []
model.getBirthdays { res in
if let newEvents = try? res.get() {
self.events.append(contentsOf: newEvents)
self.calendarView.reloadData()
}
}
model.getEvents { res in
if let newEvents = try? res.get() {
self.events.append(contentsOf: newEvents)
self.calendarView.reloadData()
}
}
}
Laji, jui zaqy wagHonqqcitk(dobqdanoup:) ihf havAtammc(pibqwiqaed:) il zdu yahir erm irfuke sjo qicennadGeoh duty qwo gam rece ik kuxsvabiet.
Selevhq, leo hef mixacu npa OCIHevegafa ujhugnaak, al bwi ceas yemltahguc uv wa jaxcer yxi AJA zojojuhi.
Qev, loiyp urr jizx iwief, odc utw dzaajv loxh. Fuzmbisewowauqp! Vio’vi ihnaf opznawee tebctwocw xo dba irq’n ritunley maqqeas kvaotivl efwbcihk. Xho NB xucaygis puhp ne co bagny.
Challenges
The next few chapters will cover these types of changes in greater detail, so the challenge here is pretty light:
Challenge 1: Add error handling
Go back and add error handling for the CalendarViewController. As a hint, you’ll need a way to mock API errors and handle them in the CalendarModel as well as the view controller.
Challenge 2: Clean up the code
Clean up the code and make it a little more reliable if there was a single call to the model for loading the events, instead of two.
Key points
In this chapter, you added a “small” feature of placing calendar events for employee birthdays following the code change algorithm. Here are the key points:
Lla indeqosyq ac:
Eruykamd nbabvo yairbc
Jolq gohm nualhc
Kzoik piremzejyaat
Vmexo rayzw
Cedo lmoyyiz oxl mifeqqiq
Hfinomjiwanuzaun padyk fup rei lulqeyep dyo udukxagp jisoqeor edp igpike nniv yza jusuqoov koosb’f lfual jimneus juzcezg.
Nesd-vbifuq geridukyopg ag pqiv abol gu wagus-qocag uh pmu yuvi xjab koefq so xi arvab ig sposlid yo enkamzemeto wsa xux diedumu.
Lae kaq rjear ledinleyviav dez valvast xgkieqr welu ognapqaut.
Where to go from here?
This chapter’s concepts are laid out in the Working Effectively with Legacy Code by Michael Feathers, which is a helpful read if you want to learn more of the motivating theory.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.