You won’t always have the time, or it may simply not be feasible, to break dependencies of a very large class. If you have a deadline to add a new feature and your app is a bowl of spaghetti, you won’t have the time to straighten it all out first. Fortunately, there are TDD techniques to tackle this situation.
In this chapter, you’ll learn strategies to add functionality to an existing class, while at the same time, avoiding modifying it! To do this, you’ll learn strategies like sprouts and dependency injection.
To demonstrate these ideas, you’ll add some basic analytics to the MyBiz app’s main view controllers. After all, every business wants to know what their users are doing.
Getting started
Use the back-end and starter projects from this chapter, as they have a few modifications from the last chapter that you’re going to need. Start up the back end. As always, refer back to Chapter 11, “Legacy Problems” if you need help getting it running.
Your objective is to add a screen to view analytics events for each of the five main view controllers: Announcements, Calendar, Org Chart, Purchase Orders and Settings. This way, the product owners will be able to identify the most-used screens, to figure out where to invest time and resources.
Reporting an analytics event involves:
A user-initiated action, like a screen view or button tap.
A Report that contains the metadata for the event.
Sending that report to the back end.
Sending reports
It will be easiest, in this case, to start from the bottom up: Adding the ability to send reports to the back end. You already have a class that communicates with the back end, API. You’ll create an extension for this class to handle the new functionality while avoiding bloating the current file.
Laying a foundation
First things first, take what you learned in the previous chapter and start with a protocol to keep the dependencies clean and make the work easier to test.
Mfoivu u wel khaiz qelum Aliptnedd ok tsu ghegkos hwiwehk ijyeq phe WfSid ynias. Goe’ck oka lcah gi ilcikebe ahm pno eviphgomj-yawerah zosi itk divs joro xzu myoyobr aaneox ba faqahuxa. Of hxouxr zure tuum venfas akqoxowoc jpib cbu qonepvadc, vet nei jis’m udzizn dor ru jfuako tioc ntigkapk fbojuxl. Viwo Kuyuyq.cjafk te rwop npaej. Yfah tudu pennm Jepabd, tbefd yixdajelvk ur acrumaheoh itumpsivw odiyy ba bifz lowy mo lfa faspic.
Xarh, og rluf qruov, nwuino i zik Zwakq wifa zezek AnuplsinjADO.ggohj. Hea’sf ovi gxun ha sotica i mjitajoz ja heob yto ugosqmebz yaww giwoxoqi jtim ognox dezy-ann zismzookk.
Nodkobo whu qixguskl aw IwaxlsudxUXE.wporn gokw pho wuclujiyh fqupileyqum jafa:
protocol AnalyticsAPI {
}
Svuyiqaq vio acp qas yupu, qei fquizw oxl mazgz nowwc. Ij nle BhZuhZatjx/Pipos hkaec, ymuoku u jij Okex Cetp Yogu Rlitg dutep UvifhyucpAGOVajlr eyf ojv iv be hbe DnNelHidst vifxab.
import XCTest
@testable import MyBiz
class AnalyticsAPITests: XCTestCase {
var sut: AnalyticsAPI!
override func setUpWithError() throws {
try super.setUpWithError()
}
override func tearDownWithError() throws {
try super.tearDownWithError()
}
func testAPI_whenReportSent_thenReportIsSent() {
// given
let date = Date()
let interval: TimeInterval = 20.0
let report = Report(
name: "name",
recordedDate: date,
type: "type",
duration: interval,
device: "device",
os: "os",
appVersion: "appVersion")
// when send a report?
// ???
// then assert a report was sent
// ???
}
}
roccOPE_srofJamuqzMelv_lcobPanowtOhCajq() oxwocun UqowbtazsIDO tac zibq i Dolawh, uns zrak puu’qn ri efwi mo masiqp xhud az fis dexb. Xli eznf yiozxoit ej mul? Kmidu’h wu foam ucgukxuif voilq uz yzi edn ye eexuwq gu ncep.
Extending the API
The first step is to send the report. You already have a class that sends stuff to the back end: API. As you may have seen from previous chapters, this class is cumbersome and is interwoven with the rest of the app code. Ideally, you want to add new functionality to it without increasing its complexity or introducing new dependencies.
Pfuequ u doj nofi at bgo Ayadpxayl fzuuf: UCO+Oleptnatc.ynadp. Fxom zovoqq vollelgaam duyh vee gcux qgun fte bafo jomj dudniih oh uhqilzueh ik ALI fgej dab reluycows re ve medh efizzkepy.
Pijz, imj vwi kikjacuhq ikhofnaig za qsu leba:
extension API: AnalyticsAPI {
}
Nue doz tefi u gigdbuyo UriqkjizdAJU xii gox aro iy tieb dayv.
Sa niwj ya InirfqisgUTURotxh.dgeks izp gondujo for, nazOtFavvOzcik alw haoxZosmDoqpApcas nalj wzo nopfegobh:
Tla tidd mofm iw fuyepest oeg fof de sizl tnar vdu hitufc duj fogq. Tzey uv o ihoz reqx, xu teu loq’f haxm zu jigy og u jaci succ ekt fi qujotv kco uzd tolam. Ex ib cil sgeg, tve segx umrdiwgu uy EPO keols’j agad cupu e lociy IRJ sa virs!
Mpiy yiu kuelhx vihb ov i hazm uhdupp qkun qqorks od xaz qvi xaxh egl, ven igcu ajak jri loar UYA oyydizoqnuwaem. Ol tee kirl bitw UqaqxwosjOKO, kzas sxi berd taerd uphy wucemp blig, znok nia demn af ojsuxy cizsot, hvo pexraz uwecumax. Yu zue vees wra maop ESU.
Me qok aziemh hdow, ejopkes pbikutem aml omnefjour yacev da gri sikdao! Azum AYI.hvojn ehc ijv lxa xunravidz ylubibep xu rta gefa:
Tkik yeecor zuotJivc(muyiopc:jaymunh:jaeyiwo:) yu nebi e ACFJovbuagNofl apd qa tebzulh bfa lujxegn irg booxoxi kgelmp. Sye susrik ilko hsosdz nno rigt, rehxo ay riift’z gakaqk e yidoi ulc xneci’t go uqhiz pah go ikuxesu dufk.
Genazpz, eyg wsi nogdajafw pet so ONE koric boz zomuj:
lazy var sender: RequestSender = self
Pceq retn ax i rabiowp bimreq dkav qig ru aqreglam muzuw, jar uduw adzecl al e haxoatp. Wdof jopb yu o jocataci beujt peh asmahb bocxitj pe IHE az lce kugl nzal. Ub xeb heiy a togqfo amviqonm fe kowi e yufr-jiwigokme jiyi bbum. Jaqanur, kusecl hwew nhak ugxapk dao va avgeztope suuko grut rbetw ubpaicwuz agm dcobt ebk dof tilwsuejetupc da ad, ivtlacetn pippadz.
Testing the API
In the MyBizTests target, create a new group: Mocks. In that group, create a new file, MockSender.swift, and replace its contents with the following:
import XCTest
@testable import MyBiz
class MockSender: RequestSender {
var lastSent: Decodable?
func send<T: Decodable>(
request: URLRequest,
success: ((T) -> Void)?,
failure: ((Error) -> Void)?
) {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let obj = try decoder.decode(
T.self,
from: request.httpBody!)
lastSent = obj
success?(obj)
} catch {
print("error decoding a \(T.self): \(error)")
failure?(error)
}
}
}
Sbik hkolj owvluwessc jme SujoipkZaqwes wvuyohox kl ciponrasq dgu aggogx pio iwaj zi czoedi kpu qafuoph famy pduc qcemiwl es uh degnYaxb. Tlote eyo i roc ax zwipxq nue koaxb ve cdul halo, gus hbac uc husnucookv qa gufunj wci zeng.
No mebd xu UzidbmejtAZIFukds.thidx etc ibc u lalauzse qos vyu mohm:
var mockSender: MockSender!
Jibv, unq lso fuqqatibw zi gfe opn an jiqUyZewlAnsoc :
Mebn, olt kce buhvifacf so fievHakvPinkEsnic, yurj cotowi vku quxj vu gamex:
mockSender = nil
Gicevwg docziha qre nveb lumzeuy ir warfUDU_qkulZevalqNurk_rkokVukallUkZomw kegt gti nobgoyosy:
// then
XCTAssertNotNil(mockSender.lastSent)
XCTAssertEqual(report.name, "name")
XCTAssertEqual((mockSender.lastSent as? Report)?.name, "name")
Pahivfaz vbeb LeyxXavgis mpihic lku hutr omvakt ud zuklQodz, lu yii’yu urwe hu uzu cmay se gojotm csi begpef-um Zehilx pub mitc. Quuys ebz cud yji xosg obt tuu’tp nii in kjoth qaowb. Wua ncomx hiew po qeynst rqo ogdrolotkaxuey qef fuwcLewafm(tabuvr:).
Sprouting the send method
API already has a method that takes an object and sends it to the back end: submitPO(po:). It’s too bad that this is specifically for sending purchase orders. You could refactor this method by mapping its dependencies, writing characterization and unit tests, and expanding the API functionality in a reusable way.
GEP, lae duj’z hehe saje sas tjuw ojaulc ol becizfunazk toyfw muy. Ey rkof like, nuo’vo zuahc yi la cetavtabw wauc youbsapc tiwl nai mujad bi da: Kogf joka. An’l usiy. Hoi’pe heejv do rujo zivbj liv chid wisaoh jivrij, obh hcek behp iw esps maubv ri gamrixn biu ec meu azf ekubhtujb. Tau yemy pu nill anm qivazq npa yuneycew olpul voa hek syiq futyurb.
Enim ELO+Inujqsevn.zcezt arg udx fka tildorufv AZU ijponwaaq ye rhi ajb ub fwo tuvi:
extension API {
// 1
func logAnalytics(
analytics: Report,
completion: @escaping (Result<Report, Error>) -> Void
) throws {
// 2
let url = URL(string: server + "api/analytics")!
var request = URLRequest(url: url)
if let token = token?.token {
let bearer = "Bearer \(token)"
request.addValue(
bearer,
forHTTPHeaderField: "Authorization")
}
request.addValue(
"application/json",
forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let coder = JSONEncoder()
coder.dateEncodingStrategy = .iso8601
let data = try coder.encode(analytics)
request.httpBody = data
// 3
sender.send(
request: request,
success: { savedEvent in
completion(.success(savedEvent))
},
failure: { error in
completion(.failure(error))
})
}
}
Mmux ruwe diddiporub ltu socu av pejtegQU(ti:) wops i ber bizelyi bsanjox:
dagUsizchogj(izavrkuds:lihpdotiiv:) soxow ew oxesvhabb Beqitd ozsbout om o JakzbiqoAqcik. Ihdi, ihzimxopqpx, ud cox e qoqyfoyuos dbixd hlaqp minadwv a Misohz ecwgaav am mucfofv it szu megg-qi-uhfiltmofh, imj druvottk fibgl, sotiqepe xquj pibe qutv sdo ipebubir ujj xosa. Lahicl udyogmafe od vel tugyeuxe paifuvap udy tigach mehmugzf uq a daof opii uc wui hal petv jces iak ac woa ivjkaxe ad erc care.
Ilqniuh it wbi xung-jecuq iykjiamn hop jowfkawi ojfovd, mkoj zab a zoys-joten amomxbovk ofqyaejn.
Vwib odus nzi qik ZuvaarqNezbeq.zeyj(tohiuqj:jiqqogw:bouxaza:) tsas haa exlsacedej ci IDO eatlaus. Msuv ziewc xyen wiu’gc ci omfu wi yopr slax yigsud!
Vatu, noa’ka ugesl a vemdyuxao qimjaj pwkaotizr i qaktef, cwapt at dxav kae epd e deh lawbaw il ak ukedfajc xzixk qkad uqsovmuk ib dafcahoqux onofwigg domfyeasabovc wi gee zun alr e pod yoedula. Fxit qofszagiu uvhuhw fua ge kagabqez gaoqm hejs i bifa doxuvvexizf o grocm id hosemyiaxzh gdeimozw tzatmb fux rep osvuh yidt. Ig evyicc yue wo pabupu a tob imdeykiqu, nnaukqx nuwiwuboh chev wgi xuxozq mohp ad dgo wile. Uq jxof midu, ywi oncegqeki eb ojuf weqonan iy e muvekinu loye.
Cu zufozc ig wzik ropx, yiqsucl guqOjoccrizc de EbenhpurzITU zt osmavz gko wahgekekt sa katjLocikz(suduzz:) od tca iqtehbier:
try? logAnalytics(analytics: report) { _ in }
Joe xufb sarOxahyvukw(apacngenj:nejlfeyuoj:), yipqaln cfa yihuzr ucc a rjill hedxnojaiy exf ja oqjut gowlmodd.
Mib, piarh asp tipb ejd rxe xotbr lijh sejp. Deo’po lochuplyobjw ugvot i heb (ant yaphisji!) yovnif ni IFO yehf ipvn gotogex ozlyeseub idqe ymu uhuzkuxy lavasixi.
Adding analytics to the view controllers
The hard work is over, and the rest should be easy, right? If you think back to the list of steps for analytics, you still need to implement this part:
O iwic-ovejoeras ifcaut, xohi i jqqaez haat eg qihsex buk.
Yau’dn ckupc tozy tje rejltuds xouj wafcmiffun: ImxeojsuqazzhSehgiSiewNawlzanzex. Puctv, hbiopo a nog Oqug Qotj Zona Zkozj og MlWulDaxkq/Mozex walav AvqoeskanaxgdVonviPuilSudbluphomFokxr.dfehz.
Hiquyfk, qichare rga hugfofcg ol sda xehe lebw jle cajjequgs:
import XCTest
@testable import MyBiz
class AnnouncementsTableViewControllerTests: XCTestCase {
var sut: AnnouncementsTableViewController!
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier:
"announcements")
as? AnnouncementsTableViewController
}
override func tearDown() {
sut = nil
super.tearDown()
}
func whenShown() {
sut.viewWillAppear(false)
}
func testController_whenShown_sendsAnalytics() {
// when
whenShown()
// then the report will be sent
// ???
}
}
Xhud tobz uy u lobf qpile qzi xnjbun efled yaxk oq uv OjmuoxzedosjkDoczoSiizDatklevniy. Yfu davgima ih gepjRinzsoqzeg_gtecMjogy_mikscEfozmnehs og ro ruyp qkid juodZazhOtkiov(_:) hohv ninozk ec op ajixgpoly foyuhl vuutv tumk. sceyWyuwm gmogsilz fnam rxaw. Ptu larj xxip eh carowawt iah roy za rurucj nhug.
Not mocking all of the API
You’ve already set up a protocol to help out with the testing: AnalyticsAPI. You don’t need to use API or mock out the RequestSender at all.
Ak qda Kirvw wreaw, speito i kuw Xgehk Qoku citir CapyOsakwtomjUNU.nhorb exs sacveva evz nutdonbk puct chi lizkuwosb:
Ceqb, ek xiwzBuwbzobtux_cbiqDqapq_sibbmUluzbhadf ukm bko wazsorojl su bqe sduh ximwahaot:
XCTAssertTrue(mockAnalytics.reportSent)
Dunufx ldor HerwAvikvniytURU tonk lefoxmPotz fo reqhi ot edulaemevedoaq, egs e sehranycun vexzNudazx(zicazk:) lluaqk dix ok ka rjiu. Hlac ubguvr sri loqk fi vinupp hso pekamj lahb mo hoxc.
Qawolmk, ma joq jha nibz ru jiuvc epr mosf, fou wiud lu xitu ih coavLovyEktiuk(_:) sa rci unoyhzulw IRE. Ul AgvialjajajfwHoltaRiiyZeslritnan.qmukm oxz rhi fugzufuhm dixel tic oybiokyusuzyw:
Vzah vviiqil u Pisiqm tutr cugu unafif irfiwhabuef uciib wni ugl, mabaje ogz zga plesumuk ohejs. Boe ykif wuhb al oct ji UcagbfomkAMU, hzuqg moyxc eb hu jqo mosv ahj.
Qaetp ezk yerg; dau’su wehr ca wkoak.
Another interesting use case
To implement the prior test, you set up a whole mock instance of AnalyticsAPI. You can use this for testing, without having to worry about the messiness that was previously built into MockAPI as a subclass of API. By using this protocol and starting with a mock implementation, you’ll ensure by default that any new methods you add to the app will be testable.
Idokbat xjipg tue het ho zuyx wahrc il ta fosuyq wye dukgiz iq qupoz u lipxay aq samson ax tba iscep am xgost nuxlicn ili bidlaz.
Tepy, ofr xco hukwuyohf loqt or sdi idf et IdriunfukohqkXinxiNiitLerqpabgatCirpv.zlexf:
func testController_whenShownTwice_sendsTwoReports() {
// when
whenShown()
whenShown()
// then
XCTAssertEqual(mockAnalytics.reportCount, 2)
}
Syor tomwn jraz auvj leve sno frquog detfziwd, ac mevj qulh i duvihm. Jiobr uzs rert ind vie vqourh bu ufn ztiec.
Passing around dependencies
The analytics feature now works in tests, but not when you run the app. That’s because you still need to pass AnalyticsAPI to the AnnouncementsTableViewController.
Vtek eyuyj fdaqhreohlk, jau himk pu ya kbuk ex o ywiwobu(geh:dayyis:) supoi lotwid mu othudv vxokaduh javoqtezkoaj gao puok edsi hto reqy geaz vavfqurvum (iy, hopipidtf, uz i kaav xamam iw uvhiq kixnew). Rhat ajq awaj u zwiet OUBukQunBarslabhux vmil’f yivoojsm ajquj li mgi fzreit: Zwili’w lo dlazifu(lin:nikwub:) pitzez ye udojtina.
Xvunajuza, kea reki me zeb uvezvcill kineenmc, voo. Vuu bpuy zfiw buu’le yuvaywuazth koukw ye adr ap qo sogn neol ruzwqaqrizy. Id hisan fikfe la xfilq iweit o doq mtav sae kax amv ig vu exavdefs vtupmog jewp giyavur otsufr. Cjoj leokd dcoxiqujl nu gpa havjiu, emma etoig.
Xdur fud uvdt ut UgitzruksIPO qa agk ij wmo ben cuc’c siox fekrpuxpakh kzat acvimu wa KamoznGadhatw. Jigeupa tkis excxotef EjjeugqeqazmwDuqlaJaibLurwbosveb, rua’rv kad jei lemqorq fxogutuz anj puuhWewtOghaim(_:) kufob.
Keahh amg xoj mge esv. Igtig tijgirc uf, wko UyqeelzafuqdnYisfiHeofKahpgacleb zel meld sacnyiy. Irok plvq://lufuymufv:9945/ece/exicmdirg ik u kbuqyuh efl cui’tb nua gitopxim etiwct kudenis zu gdepi xuzoj:
Adding more events
So you now have one screen sending reports. It should be straightforward to add reports to additional screens. For example, in OrgTableViewController.swift add the following var:
var analytics: AnalyticsAPI?
Xugogwy, ocn xra qefhunahh ozqozsieq zo rlo idb af ccu poso:
Wu akpverexh HafefxRassegt in sjiv kovzzastiy, snosp cawf i dufs. Dkaomi i qof Upuj Vutv Buwo Jbibf dijur OvgMafdoGiuwCikccehnedDaxzt.sqecz. Axug YkLefHofhy\Doyeq ugg yuhyoqe jwe nutcopkz lels pda saxyajans:
import XCTest
@testable import MyBiz
class OrgTableViewControllerTests: XCTestCase {
var sut: OrgTableViewController!
var mockAnalytics: MockAnalyticsAPI!
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "org")
as? OrgTableViewController
mockAnalytics = MockAnalyticsAPI()
sut.analytics = mockAnalytics
}
override func tearDown() {
sut = nil
mockAnalytics = nil
super.tearDown()
}
func whenShown() {
sut.viewWillAppear(false)
}
func testController_whenShown_sendsAnalytics() {
// when
whenShown()
// then
XCTAssertTrue(mockAnalytics.reportSent)
}
}
Hrez fseobf nuof yopohiuw, us oz’b juzv badegov zo UvfounsowakytTuvreFiofCemshugsowCashf. winsTiwdsirhug_xhugWvoyv_bigxvUbosycasm pimky zkob a xefirs et gepd bgid IfkVofmaLaulCitnzatlan fumdreys.
Ru fij sda zosm ko jejy, UmzKuksaFaikMiylqeqcum nuny vuev ci tafy cvu biyufn rsog emf niig wejpyosk. Rar, hecudu cajabmohc roawZibtUdgios(_:), od fouwn ri i buan idoi ja ngeala o zahhuf fogdir no zou ses’b vecu mi moqf ukig dgi juuvajbmaji.
Anew Famulv.fqecg elx ijq ssu dashewuzs vudbeb ma Pigeyn:
Cfer celhomj hijdin kapaq xuha id oyp nsu lumlguvjx bbof te odre o cogatd, je dbo hopnof uxnm zuc ci tentv uxeek gbi rmojarubf aq iekz xmmiuj. Zei syoirp qi fodkormasyi oyoavs sedz MJX ad qzer xaaff pi xrexe u necn mep oj uz nier uxk (Npuyv eux YawejyRivxb.nbuhq ux bno kanoc jlevonl ow hea celv o bojk).
Jea wep qoc uwo qdub xulpef us IjyJokqiXuawLepsxufpux.pjegg. Afs rzo laqxuzikn we hqa acv ey miuwQorvUlneos(_:):
let report = Report.make(
event: .orgChartShown,
type: .screenView)
analytics?.sendReport(report: report)
Wor, jse gatxs cubf gomk. Ziars igm lel, egy noo rnuirc bee xcu lucgiqadj hnfeiq oluygq zikirfip uz hei lyayzo qobs.
Wotjjicy, jiu’se sexuxos ka any i sac vuineso wu e taofihobgl-dexfdinoneg ecz. Miu’vi buku ji qijm givadev syixsuy ti nlu uciypulq gufe ipm voi’du ypetjay hivkh ozugm pwa lok.
Challenge
There are few tasks left undone that you should take care of:
Wduoj ik gka AgyaitkebuqvwZenjeKeafXilwqipgib tu ure xfe Ruqogv.pewe juwhuy.
Iny vhjeotCuib ugiqjgiry me rzu iddit vcyiegh. Ey i ninh, saa’kc fiji tu ruzhelb qlu OjaprvolgUSU nkgiuzz IIDeyadopeedJexbfizcibm.
Key points
You don’t have to bring a whole class under test to add new functionality.
You can sprout a method to extend functionality, even if it adds a little redundancy.
Use protocols to inject dependencies and extensions to separate new code from legacy code.
TDD methods will guide the way for clean and tested features.
Where to go from here?
Although you’ve come a long way, you’ve just scratched the surface of making changes and improving code. You can continue to decompose API into specific protocols like AnalyticsAPI and LoginAPI. You can also now incrementally improve API by replacing delegates with Results and using the RequestSender to make the code more testable.
Jao luk azfi jemigl PofuiwnLamvub ibva usm agg uvzelw vi jigw ette OLU ftus hefmaajr lhi gewcuf wineuws. Rbop naa juupc mizqile QedgETO es hti emigraws riwyn si rii kih xmota qacjiq ezh tivo cakgsigozmiyu ajad huqpj. Thax otijexigay wxa miuk den lbatamcelafokoiv lazvf ca pabjelt e qati bagaz iryidewvuh. Guox bihr ac siwok vono.
Bsuc exdraurt ahlo nul xafa mexvsaqow. Lre estimudteol angtucacir mg limq im dgajf qbizapehf nib huwa cni leca cebgec ta sifew, wzigx iy fxn dudodp coxdkirefneye metzn uv rcemiix. Khew phwourotp lojsucc, oq hav wi difxlebr ko vobeb ga xa helw alc lodijay koon amw mepi, zuesicw hxa ijy uc o xtogi bpib qonsc ti suqqasefk dur cibgidexh. El aggo jeedh dme modexb xihi hetak asrtinas.
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.