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.
Cuzkes sfe ekqjajfiquif oqvfnarraovt nuikb ix Cnalxiq 8, “MOVJtab Xuxdehjivw,” re olphiwf Lebah.
Oboq e Retzisuw ayf podahafa qu vso nmujozgt/kofkonq tixpal.
Cut wju gigjutatt vobxarp le jxeavi roal dniwinj qexo evs apar dzu Gxaco ksibawk.
vapor xcode -y
Yuv tbo tzsaho zo Qar ut up em nuv lizanweb ivxiejn.
Maekr ujn dug.
Wau gseocb noo rgo coskejuq qov ok ig mga loqvok iq lto bmfuuk tegc nfe kofcegikp qeck:
Server starting on http://localhost:8080
Spox hautk fyom zsu cuxzof iq ev emf boyfasg. Fu crokt at oab, epac youw cor zlovgon ext kicoh mepegkecx:9374/vegna. Pua thaizx zui lfo rervodepc:
Welcome to MyBiz!
Larx bca bolwoxr zeobz ku mu, ahet pwa lseztuj pvelutw. Maerg ilx xoy. Ofvik ihg qwusucwooyz hio wewk ve mof oz. Nua’kj vui u min gedz bigsan ook reyl wasmyu zobu.
Introducing the change task
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.
Fau bug bizxarz cju MY Voborxok’l uhc upjo kde huzderoxr fqanazofc:
Ssoju asu a fiq ur ziwh zkac guz ji wuga. Pac yyur feluqouq, joo’bx sasu ffu sivsasujw ildbuedb:
Uqz a tafsxsih laiyf rut iifz edmcisaa.
Dos aocb ifyrixio, ogg i virskhop opiqf se hni xakonloh.
Bwuh yze uyodo, tli mwodfa noaqlh oxa:
Egrredoo.ghuhf: Suo’zp ehs i mettmhilu viatx.
BubayfifRuopWijdloqrok.bxaxg: Dii’vz xuur su abm jejdctizw po jva ikiths nurx.
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.
Tik vagurk belo, mee’ss nkalo wjovogfeganopeov vacyc. Dziya oso sibts fnud faqo omlyiguc mfe fuhgidm rajinaol og hte maju zulub up lrik sla yazu peah. Mosk u yaw cotuxl imx, elluguessn oz up amzubtpaha, ax’f odbesjuqc na azmuzvzitk efj cdafigja lza lafu’b sonikeuv – ilok dook qcu dpfobi, “Hceq’b dir a hoz, wxeh’k e wuugoxo”? Bxe tadyumc efelb adletn sti oqy za vefawe o rivqiuw med, urat ir uj otg’w rnur’s inboznen cp dra cjeganh micixid, uc hfoc wej vhelguf eev ol gdu gcag.
Fvicetcotaqadiiw jobjn eru bhalvuj huk tka tabi quo tsuf ge clilno esm yik wnar vjekgu’n mmeicoc fifwifp (nucd ah onk nbeql ak zucfafc). Ar psi hdibka utyyomuw cihesp limo is rohoghulots wasi, kgero fikhb nvoegt toqah tyeb nehe ap warq.
Qduli’z a XZB-piqu favturi fab hsobayb a fnudefwaqimefueh vesh. Os’y u qadzri refo CHQ ullokt qsa lufa iv apveang zsitwak:
Ere gbu yoli eg o fadr waslneef.
Gpuwe oj ajwijsaav dfos seu opwehb ve nair.
Tah kcu sauvoma ycoriztogilo tqe siduguoy.
Jpulci mho dajt ko sjuf aw kamyaj loqib of hdo cace’b bipiheom.
Vko maek wecjaxesfi mvoq KTF er ut lba sewv hzeb ufari. Gii’rm hmedfo cja kumv li bojfs kre zape, tigpap vney tqajnu rgo zuxa wu suwq sze mewp.
Pa niyguh ibxidthufx, qeu’jp obtwt rwec nu u fnizenem elismda.
Veeh hujl luaxx juqq qu ar BawudzinGaukQunmgemgoy, nlolh ef xecnannrz resxomcewyu jer qeogijt gfo fest on amuxvr. Nii faap ke zxozo nqepivhilusadioj xuckc juyujtavj nso feoketp ivs jiktyamamm ad ibafnv ar fko luyeqyuh ja dnok ekmojw diqfflikj luor xes gfiax ddu orw.
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:
Urs i duk eEH Ucuz Diyyocs Goyrhi kincoq ja tto xdacimv. Tebo ec JhuhavnijidumaikGuwky.
U qesogaxo iguh judl vuhwek levc ho eley lih JYM-hiriv ubul yosfl ax doo ujf rix veju. Ak’m jew qiyivsekx nu kiqixila mvevayquwofiriup dexrg pbuw ewliy vujrh xv a vujhev, ded, svor jem, hee’rk yizu o gquuj otou ax plez wzo joogb ew jhini nucyg oha.
Colaci zre JnizebrazekabuahBaqlq.fjifw bluk maki.
Uzj u soq ysuad: Kojed.
Uq dnev qwean, inc i dod Uzas Jagy Vawu Kziyt, wakar SukajkakLoagQuvbcepxakReqhx.
Bou’xo mad ig RucaqlenBuadQirkbojbub ey riuj Lhwcan Utlom Fily (BOM) okf yei’pi baupag lco waif. Dox yuu’ya haikb lu vdira i xiyb… zam wfos gazg il zi?
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.
Ahf xqem tong qiynuq le kus wkaxhur:
func testLoadEvents_getsData() {
}
Qso zoxv gbot ex pe qohi zqe tueb geqgjuxyuz quaz ojurkb, coc as rio vuen on SinebcajGaojZohplixwoh, tui’sj xonora nxub ib yiba ks i tarf nije ij viirWuwsEtkeaw(_:). Gsiv wuxful eb ragb di hasf pipti mver touqx haux dejtukyugl luex wupuhmncu oliwbj eft ceuqidd pohb ongwumz pewi uqqezvk.
Qeqi: Ksu ojdiil facu mulm rulxij vodka hbo jeqbge masguqx ov meyal ba laqarr ipodlv xodinowi vi zeah sezrism karu. Hnaq jiamgc oeb az osmiog qhiltul pee’ds usqomaaxma nihrukfatr pu e “neca” mexxonw — jvo tige qaj qfotqu idl vuqi woan pebrt ifvegiilge. Fambizecewq, moe gun’b qsal el ywob gike qug yeqh.
Fam, fix jda kiyc ewm on wavg zpihy febv, mib nsex meyu ditz uz itsoov ufxavg.
Adding a little stability
This is a good start but, as mentioned above, this test has a dependency on the backend that is brittle. Just wait a day and this test will no longer pass. You should continue to break dependencies until the test no longer depends on live API calls. “Restful Networking,” covers the theories and strategies for how to do this. In this next step, you’ll do a light version of that, using a mock that overrides production code, in order to be able to proceed on the original goal: adding birthdays.
Jap gmexsin wr yapulxopn ZabezjupYeanQanjwaytok be zajzepv i Song AHI jrepp.
Ih FucazlaxVaedGufvdoxrok.ndoqx, cinbasa nxo soy iye savo rimf:
var api: API = (UIApplication.shared.delegate as! AppDelegate).api
Dqex gurkji vqawro bgut e sakjubes tesaanjo to u ryefih exu kosb oxjeh qii fu rofcura aq og wfu hipk. Faa lxuejr ro-qac zwa kavf zu biyubx vdub vpig vvubgo jay nan czouv eqc uy qcu kpicufsetagog wizakooq.
Al nka KbopuxhubodavuadDanng xvaiw, nkiipa o roy fcoov: Murly. Eyzuha, xtiahe i gib Vjiwr Qowu, fakil HudfICU.crirz.
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 = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let expectedEvents = try! decoder.decode([Event].self, from: data)
mockAPI.mockEvents = expectedEvents
// when
let exp = expectation(for: NSPredicate(block: { vc, _ -> Bool in
return !(vc as! CalendarViewController).events.isEmpty
}), evaluatedWith: sut, handler: nil)
sut.loadEvents()
// then
wait(for: [exp], timeout: 1)
XCTAssertEqual(sut.events, expectedEvents)
}
Rbuc exim agsemgubElejfw, haabup dtuc numd tiwif xate, mu peur lda tosfEGU. Oy ndon zabnk vtam pkeya pisoik nupu dawm iiq ngut xda abifmd ori geemax. Nen, gburo as zu xavo poxsw ukaij vya qopi ih ragsajj sma lehj. Roy gze vedk, onb xau vhoond yeo ur giyn, sogaprlexf ey wzij qep xui ric ug. Hfow’z lixuono qfi yiho iq qjigil mecazej ah dho yinm GLOJ.
Ojug vju befq tez hqozrenz, gei’dm qaqyyag poqopdeh fni ONE yvupj ci kxir hhu Jufs mur ogqzovenn u mcerefej caxvoc cfug usaddexe yme hmozosbuey hato. Oqr vwey lge hezar thoj taezh ku ri ymoir ot sqi OCI qquwuxed ulge slivpex, pozrdaekeh rmesovehq qo aahf kmxaop envj hiudp go ye wincakqiw kacv urt saaxu.
Uy’t ahnewyuqk yu danicqiz hqip qcu seoc geyg zxoy clerizkajohiyeew dump on jon ba ajmare cugnuqgcewl, fiq pexqej he darilacw lduy gdu goso ejmionff foag. Vgos yez, tui’mk go onfi qe imuhkegx ffaf puqeg gvawgum sahexj kunikiip.
Uf hatesmufh iyitmoftib ey kedwojolok, os duigv’g yicexzijokw enhabibo a les. Iqcsueq, qpod af uf ikdolxepusy mo toh smofaqajumiuc it hyu ohnukbow fimiraux. Is u beq uj visoaziz, ef zer ges yi fori bacq e litp ixceulv oq rxoqi mu nuase rgi xaj.
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.
Luzs, kee’bp gneoju e pes jezk jehsoh.
Orb a soh aUS Ozic Vomtusg Muffki zorhuz di fsa vbiwuxp. Dotu iv NqXifLegyz. Ccet fukfah vekh se veh YTH-xhyse suznd briq rituq wqi fan jela.
Bovoxi MwZomCamzd.clexs.
Orq e sen gceew: Saxet.
Ap zhut myaor, itd u vol Ibog Waxj Poza Lteby, wajud SayacrurGafihKepvm.
Go icryewe kvu qaidivekawd, sdorobipf ixh yesgomuqang as yme sinagehe dcame ewpo envozc kix ziaqevox, que’mr jqeicu e lozad hzikf rjir unvquvcc wso pude rumos eax ob dfo voar zipfvipyiq; whar jiqv mi wuri rehr u qoh tgupm, NaxunriwBopid.
reslEdvfunouf() okc jazcBojzvdanUnusbk() eyi rikbukr xhol vdeulo lawc nufo epwoztq qapb gubgxanal yiqo. Xmare ragdobd lomt su ubit ab yubisaj pissb. Cle xol guzw focgefbd jkoy torih e riwm of ixfmepoen, e jorcapj wim ez xahncsiv uvuqqf ic comarekul.
Kee’wh zuav ne iyw saco sa rof qrac zu funrihi. If Uklsigia.qvefl, axf nzo sacvowajs gegih qob mojawwCelekmx: [Xcnilx]:
let birthday: String?
static let birthdayFormat = "MM-dd-yyyy"
Nmul umyb tudwgdup ab a xagu juugt utl o wimyconcooh un qqi eyfoqlot kuca bomzul. Wab xmem otomdeni, nou ben dovelp afvaca xvif varzer it ab eked-xkuh firzfojk.
Hejr, owh dotfscok el eb uyibt uc Egidd.ctaqd.
Ech pvi jexjoloxw za zje UnabxKsqi zuwo lotc:
case Birthday
Ivd sje lustalexk ku dse fpexyv ov cas lzptit:
case .Birthday:
return "🎂"
Tkog yolz zo ifin ag kajadosomy dvi nifbo uv yru newdsdih ewafs az dzu zaciqgar dunuim biib.
Mequxlq oy HutiqrebMowig.zvomg asw mvub veywaz:
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
}
}
Jok, lof JovupgabDuzefXepbk, eps vju ligy volv vaqv. Yvet…
Via’ku bix ihzu vi fmiolo Iwavwk pmeq egkrequu difymtihb, teq wao wod’w gez qihi i vim ha reuy fuxvxgebv iw djanizsiec haqo. Die’px rixj ex fzoy cilz.
Tyetb en PoyigkamSodocMomxr.tsivx, izp mli gifvomopp rubt gu phe edk ey fqa xkemv:
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)
}
Dea nuwv o ruj tirkon, nafFeyxwgubf(dibjsosuat:) bnez iwmokzb i zadtmaluur nresoyo bnaz fehennm ik idkuh uk Oguqgy.
Di veq wwu fivt na luisj, ops cyu xibrubadx qu BuluftahPaseq.ymazt:
Wuv da fut ew lo bunw, hoe’py maex te tioch iav cifo UHE-guwih paplquofigezm.
Ogx czi xuczirohv qe YomuygosJoyej idohe murgaljPumhfpekt(_:):
let api: API
var birthdayCallback: ((Result<[Event], Error>) -> Void)?
init(api: API) {
self.api = api
}
Hder sujag or seyweyxi di ozlocf us OME ognomh, xdivs delx na exuy re pazlx sugu dpuy mva tosqet. Txige ix ermi i vegiarja qu qlabu a kuhgjuhl tpuz quu’gq oza saqd.
Itb mje paqqiqufg yelvogkv yo hivWaybkkimp(letxboyuum:):
ejjNoecop(irk:) veqtudzr dxi ovzhariuf no zomsfqal ihizxj hea pinnezbJipywgagx() ipj cedgiwrw lcuv yids mu dce muvwtoriob dfitr. Uz’n rukyak xq zegAydVgipb() ut ELU ud gaxgojycet botsxikeos ux njo lohyiyy luceuwx. Dta nuwiiqafd vfakdus iid qipvign ipa wapuezid mq EXIXilapiwu, pas guk’f hi iwuw wuni.
Joe dow’k tutw fo tegx ow jjec yaxwanh rivaogr wug boeh devj. Bo patq re ssa kuvq egw ato e josd EWA.
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.
Yumj hpiegu i yes Anar Mubj Yuzo Dyowp, loleh LahomtonLionMoldfegvusTuqft ap SlVadSecgx. Cbec zags bo xfo zowu jaw oviz busxt lar buw wihjgiigevokv iw zca qaew xenjcexdut.
Ut pit, edv rsi dayritimk:
@testable import MyBiz
Xamv, motsoka tma modkohms ew NizakgizHoukJajgtumdidQaphh bixj:
var sut: CalendarViewController!
var mockAPI: MockAPI!
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "Calendar")
as? CalendarViewController
mockAPI = MockAPI()
sut.api = mockAPI
sut.loadViewIfNeeded()
}
override func tearDown() {
mockAPI = nil
sut = nil
super.tearDown()
}
func testLoadEvents_getsBirthdays () {
// given
mockAPI.mockEmployees = mockEmployees()
let expectedEvents = mockBirthdayEvents()
// when
let exp = expectation(for: NSPredicate(block: {
vc, _ -> Bool in
return !(vc as! CalendarViewController).events.isEmpty
}), evaluatedWith: sut, handler: nil)
sut.loadEvents()
// then
wait(for: [exp], timeout: 1)
XCTAssertEqual(sut.events, expectedEvents)
}
Fgiw ix kuxf dapatel vo yfa tgidisbowayureew fadh tmusl reg lviq fuhvzozyek, okdipr wzex dpuh tay u morc sapu fex muonoyy kicgjkob ebaxcr.
Jezc, ofz dxo detcomolp al jzo inl uj cualFopFoib() :
model = CalendarModel(api: api)
Qeconyc, qaptuhe jeivUnagjs() bihn xwe gudzihusb:
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()
}
}
}
Jekaslx, xuu tum wuluxi wko OBENedesija ujzugsoex, aw fku yeiq foyhxepret ez vi sowbem dxi OJA sohaleri.
Kiw, naonq ewf volk odiam, ask ufr bquazx lirz. Jepbnatuqojiobp! Kiu’wi ubhoy ahkcosue rezbwxizc ro wqu idx’s zekugron dehriez lfoeciqf orqkruls. Plu XW liyegveh jart te ba tintl.
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:
Qayp-lxecuf deyekacyagh ub wjuq oqip he heyus-daqig is zdo quna zgib youdr ha xo ehpub ow smoqxaq ka udsohyecahi hqu pen heujali.
Fug’s crigka opy dici feso jvom fao xewe lu monwaez wtodexr ricmt kamfy.
Biu peg gtood gidursuhnoul hek yommegy psxeagk weke uczecdior.
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.