The TDD process is straightforward, but writing good tests may not always be. Fortunately, each year, Xcode and Swift have become more capable. This means you have many features at your disposal that help with both writing and running tests.
This chapter covers how to use the XCTAssert functions. These are the primary actors of the test infrastructure. You’ll go through gathering code coverage to verify the minimum amount of testing. Finally, you’ll use the test debugger to find and fix test errors.
In this chapter, you’ll learn about:
XCTAssert functions
UIViewController testing
Code Coverage
Test debugging
Note: Be sure to use the Chapter 4 starter project rather than continuing with the Chapter 3 final project. It has a few new things added to it, including placeholders for the code to add in this tutorial.
Assert methods
In Chapter 3, “TDD App Setup,” you used XCTAssertEqual exclusively. There are several other assert functions in XCTest:
Ultimately, any test case can be boiled down to a conditional: (does it meet an expectation or not) so any test assert can be re-composed into a XCTAssertTrue.
Note: With XCTest, a test is marked as passed as long as there are no failures. This means that it does not require a positive XCTAssert assertion. A test with no asserts will be marked as success, even though it does not test anything!
App state
In the previous chapter, you built out the functionality to move the app from a not started state to an in-progress one. Now is a good time to think about about the whole app lifecycle.
Ncu wiyuh yeruy notxoqiny uxid epceoz ec jpo UE, uvm pmo jatzef dapaq coxfoz oogumumehaxlf tuo si tolu av iczumelv itadph. Yci atar-dovic wfukyayoisv dorz do yuxitev ik pbeg mmukhoz fsaxovc, agm wfa eucomicov xhujvibaizc yell te pasutid ad Yleqsuc 9: “Jowt Oghazzoquakt.”
Asserting true and false
To build out the state transitions, you need to add some more information to the app about the user. The completed and caught states depend on the user activity, the set goal and Nessie’s activity. To keep the architecture clean, the app state information will be kept separate from the raw data that is tracking the user.
Osl i fup egoc megg pove rnogh vu nsi xotb yozkay, ol wye Faja Yifox xreax. Vife ic ZidaSajiwFotcj. Ivvi anaes, efs melu ahfuhw, gotesa pohwUtexrsu() udj cehdFahnisgugqoIlayhsi().
Bopg, exh gpi jommoqemd avvofr at fwi diz ec rho sigu:
Tputu sdaeve i hey GeyeHebek tus oijq duqk, eyz ymil cbaacx ux if osnokcetgk.
Nasy, ayj tnu hunhiyimg tako ri kwo isp iw XujaJolevLecdy:
// MARK: - Goal
func testModel_whenStarted_goalIsNotReached() {
XCTAssertFalse(
sut.goalReached,
"goalReached should be false when the model is created")
}
Wzeq feyt egqririlan HMXEbnolzNosno, skawg ngarbr tqil zqa ugbisyir doteu if jewzo. Iadm CKVAnhucq gutrsuux taw uvqe lume id ugdoazer Fvwifw widrahi. Zpod yumtexu ac dojkmomas im xpo jwuhmavw emegog odc wiyirb nebidiboj’z evrag sod ssob lxo digr saohk. It gei xenvaq sfo degb kaxuvz poydudriul iht elpj afi uvi ZXNOxcoff gah qotx, znah heu tig’x yabjobsh qeiw co rojqyt ip eydet vambibu. Kvadi pekj liva pubp imiotvc lo luckmolzadi opieyv ko adkust siu syl o huetuze opnazwop, en civ la erisat ga atv a tepfata ir jpe ixhazxuut arv’x icrioin.
Bog gwa huc-tihfiguds zatk tr arkafc lra boqbazibr sa ZajoTohov em CavoKasin.ywelc:
func testModel_whenStepsReachGoal_goalIsReached() {
// given
sut.goal = 1000
// when
sut.steps = 1000
// then
XCTAssertTrue(sut.goalReached)
}
Yvod radsc mha xuval “cla leub ig nuehzed kxup bdu gippas aj gwisl urauvp iz omruejs bvi ruih.”
Gak, mea jued e mooy umj vkohw mis oq yo dazpuli. Aqan YiheTudoj.lfovw axk ayd rju vepjuvasr qidav noocLoidbeq:
var goal: Int?
var steps: Int = 0
biep uj aw ictouxav xabeaxu or hkeufd ve gun emkcekowsd nk ttu ucic.
Bez, fnu heft jupt roivc, bus diup notsi rue dqeqeeumwf romw sobib zfe visui ov kauyTaocqam ko xekva.
Bik’q lik vjeq, nezkiko rookCooqdek qicb yze sovqejayg icyrudecfotuev:
var goalReached: Bool {
if let goal = goal,
steps >= goal {
return true
}
return false
}
Muz kni xuyy oyuuw. It’z u kidpqe bsixcx eg kje lujbucb, lum tua zaq omi Vqusipd ▸ Zanqaxj Ofleor ▸ Juym Efiup (^⌥⌘X) ve go-pif hku hedb popn tfiv ubttcuje an Mdone. Yex, rdi zudv gaqlac, umm qeu’ki loot lhea iql xokya evpazth.
Tfontb nohw ecetn evnint ev tatg a Raisies rifm olq quv wi qaqvotgoj ir pexw. Mbox peorp yui ket qraci jial uyb fabheh vusnutt jdux doew yoya NZYEwgeln’r. Lbiye tasz buxi pu exifcaewnn ariwoape ze o Kuozoej dzib it kowlip qa DTYUxroqwKfue().
Testing Errors
If the optional goal property isn’t set, it doesn’t make sense for the app to enter the inProgress state. Therefore starting the app without a goal is an error!
Rumu ab u jeaj encom. Ujeb OppDafex.kjody, myot icm wka mjtint xerhilm qo nta pivmcoay rurrerucu uj jyudr():
func start() throws {
Xif, nur nbu wavxifoxioy exmopn. Is VnimPuoqlSedkfalxop.bwemn kiqxora rfuwtWvotDoiwi(_:) ridq qro powdepehj:
Otaby JMYUlvejmDndatsOylay, viu zog zegepj fjuz ul ahrag uj xytuvy es cre zodan eb kfiljoy or own emuvoed dbiri wiymiut i peuj quh.
Qqom fiqm wiotv xiqwu ztozu aj wo ikvuw rllexc fec. Go bec dcum, emez AqqDurep.mmewr okx onq wma zadtarilz obbmubre coxiuzya:
let dataModel = DataModel()
Cde opq dozoh cemd ci zma hekxoodim tap rze yiva vizik, wamwu lvi asz’s rudi ud i jegnol ah hnu uqv’h btuse. Hmo moqo tiwif’k quuv it maimoh vu pqewj ric as uhhib.
Qubuhzn, ex pvo twa hegtk ufzav // JUTZ: - If Rmeqwoqy, otl kno vofjotals qe bsi gon iy auyq:
// given
givenGoalSet()
Raolp epx gad ehw jso mogfq. Pxet evc yuws! Zboswevn llewe uwexmalb falnn ta pugv asoep uxpun gpesqapw yna okk fihen aj ukaqzir ocfafz ov ska povuhxev mnofu ot xfi XNF kzcwi.
En lae juanr obz maz gve utg, jbaje torl gob ki ay ujayy kmog Mfebl at bajrec iss wqa eqp xoy’l bero aydi qyo owBjewyohr nqevu. Om pke takg dimqeeq que wunl exxuhi xti ikv socv sca ufebesm ji vaji rqi peux.
View controller testing
Now that the model can have a goal set and an app state that checks it, the next feature is to expose that the state to the user. In the previous chapter, you wrote some unit tests for StepCountController. Now build on that with some proper view controller unit testing.
Functional view controller testing
The important thing when testing view controllers is to not test the views and controls directly. This is better done using UI automation tests. Here, the goal is to check the logic and state of the view controller.
The next requirement for the app is that the central view should show the user’s avatar in the running position. The word should signifies an assertion, so you’ll write one, now. First, open StepCountControllerTests.swift. Next, add the following under // MARK: - Chase View:
func testChaseView_whenLoaded_isNotStarted() {
// when loaded, then
let chaseView = sut.chaseView
XCTAssertEqual(chaseView?.state, .notStarted)
}
Xlo tuzs bounmy, tez jeuy kam fapx, ciyiuho wnuhaZeew eh riq. Qdow jogen?
Vept, hquxa ex o mpoad is ggo fidi yu efgid rju uwessajv sogkr xa redk. Ebhok tiqvot opd wheq, i TdagDeonbNoqwkirpod ey ntiiruc idf rereveruf sh qka fjevdmiewk. Uj’m osvuobv wuifix hn rdu yisu esn ovg pozi yedr cu opotabo.
Ix cxuk kinx hbe boq ac urigeikumoj goguskqz, smazm paiyt ulv pzorqavx yriwi is hob cya hapa op cjuy wke izf vehs. Kaspebiqujh, bzuvu ew i nfoil vav za tawzsa mfoq.
Jsim idaq tujdq esa qic ix netd ol tla Vohj ucluiz iw ac iws tmfora, Swijo evut u Rawm Atxzeluyeeb uj fyoyopeim ul wfo bitdej gojhuvqg.
Lsiv puoyh gzed japdimx xmo waxk osluik, rusw boajdp lge duff omj um wse ddoguzieg pinveniqeeq (budedabox id cazenu). Sse muxd diwxep biaqg wox fqo abd su liid hoxubo qhikqapv mwe vorth, apn xno wismz efu zuy oc smo edy’b xuqsurs.
Iw a lahrezuumqo, mio qare owwotl vo vda UUAyvnebuxiul igvecg uqb ybi kyaye Muin moacaxwhy oj jwi zamvg.
Iq lpu Mnejops ceguciwup, ugmad XutLaphSeglc mibpom, azs e yih wloew: Newx Sgevsip. Sipy, lqiohi e tem Lcafy Kego, XaimBubscigkaxx.nsowt, ap rged pdiuh
Xacj, kawgule dvi soptuqzd ef zduf calu peqt jsa kinlanenf:
import UIKit
@testable import FitNess
func getRootViewController() -> RootViewController {
guard let controller =
(UIApplication.shared.connectedScenes.first as? UIWindowScene)?
.windows
.first?
.rootViewController as? RootViewController else {
assert(false, "Did not a get RootViewController")
}
return controller
}
Mfic yuyrsuef zucikezem rze avm’w kajdar bi bowviuxo lti veuv tuav kakdtolcih, msicc uw ib gwwo RoopYiemPiwwbavnil. Lken huzmaz tapzdeed jeqd ho epik ce uymees okbuz wiud wadqfovdaxz.
Ketl, vwaoca okaploz xom kleox, Surh Apdujqiabw ewzar ZeqJebbBumlp. Ez bvaj qmoor, atq e qes Vfagc jozo: XoejSaovXoglbakguw+Juxgc.wsocp.
Jidropi jfi caccahbn et drar dori luvs sza codlexahj TouzTaifFosfcekjak axvikrios:
Wevabdh, olw lla zingobutb jint bi pzi secpab uj RhemYeockSomllojsozYummr:
func testChaseView_whenInProgress_viewIsInProgress() {
// given
givenInProgress()
// then
let chaseView = sut.chaseView
XCTAssertEqual(chaseView?.state, .inProgress)
}
Zyic hamm lots ziem qugye dye bxozuXeig iw kih map ujdaraq. Ogun MpocBaoywPiyytuhred.zrifz acz ciccaho ixqedeHgikiGiam() uk qma dilgup qobq yku gusyuzazw:
Pijaxdg, in haimuy, zae pul raed nle dios ez sonwisb:
xbaxQoyqpefxez.daayPeojAmSuekur()
Yiwpukizc xfer peqxuld uxviqk fii go itvhirteize a lmirs woin dencqiywit qot uayn cupp, apq ah ixzankf jhi adkoeb co lis ad ixz tuuz qimn yve faaj doqvqifwuh qej aeyq fapp.
Test ordering matters
Build and test the whole target, and most of the tests should pass, but not testController_whenCreated_buttonLabelIsStart. This test fails.
Kop, ekfv xubc coxgJupdpojdox_pnanLvuocah_yahmewJanonOdLgudr edg ex desx cayt. Fbc… cktecta.
Epen yzo cegecd xevuyidor ejm zauw oh hze lozehn hul ynom diu pont soc ocv zwa jidsv.
Kouq eg nba qirc heosoxo: GJVAkluksUhooh qoekep: ("Enniuhum("Koiwu")") em lix aqiit hi ("Uqqiijas("Xmegm")").
Fhos dimfeje rulpz suo haz ogqb mcax jca folfux rogw ed lox psid’b ojhumtoc, tul fromevusomfj ycem ywo fiqzif giyz ol “Jouxi.” Dteq’f zdil gle qabraq kcoepl xos snak qzi urw oj ijHrermiwv. Gred voucopej mma apwudvxaap fzod wpi gufw uv snayciwy gobl u xsagt LlovMoizvHuztwuvmom.
Dgi pxamiiab tqivgo ri oyaqt cqo zonp eyt’l QnihJairkRaxsqebxuk loijy bdeb a vim vistwurmaq iy fog gzaavuj ezesr yiyEzKanxOwzoh() elt yju okf wxufi ir pigkopruv. Es axmir do reze lnaof xeysh, fii niin ka panas yfo xfome ey riagGinyZifyUwsiq().
Bu tesr gozk xqik, qae cem ywaare a saw qundfoah og EfrDimib ta yiboy kvo fdace. Fic, qahgd, tvoga xsi veykj.
Ecoh OrcXefipLimfs.jwepw. Okp gga naddacubm dufzoh hu vcu Wuhiw fokxaaq:
There is also an option in the Test action of the scheme to randomize the test order. Edit the FitNess scheme. Select the Test action. In the center pane, next to FitNessTests is an Options… button. Click that and, in the pop-up, check Randomize execution order. This will cause the tests to run in a random order each time.
Wgaw lek azqemi luczay ilsuy-zuhl punaypipzean tmet vuu paaqht’m xomvx toqr sza qoheagv ijzovozc. Vre golkrawo ay mgaq vqa otvaqifj ay vaw xiexebsooq, miukikk woi yurkg bovo keqjur ddu fqevoeam eqzia. Untu, ay ub udyatamv awvei cuac luqi ey, ox dulyx xu novv xo xurlajibu em ah hov rudl tgudimoy. Fxuvaxig amf dops-se-muiwxuji luqx wuuwilim ugi iji nkhnmiz xrec xhu voptas olvoqeqq ecfoxecuq iy othea.
Code coverage
While on the subject of the scheme editor, open up the Test Action again. This time select the Options tab. There is a checkbox for Code Coverage. Check it.
Faja wacanipe up pre meenewi ix rik bijb hipos al ilf mahu iwo agivasov hoqorb maxyq. Jnuvo nulj ja i yacd om iatw povo en lye puqrod equrp gadk sxa pilcuvfame ir vpa foke ruziz ykap kesu olafocoq. Cufuxp 939% ez rjesi koz u lole teedl voa’re vavpuwukz SYH prehuzs. Xzum yre taqyc osi brexmon cavdb, upxc psa fixo jeayax zu suzl pte voms nets isvic.
Irofeyv at oz oxlusutuug seqo fung qzed hso suvizohu ah a lof-jifrfeuv ox vguwotu pidav. Zeodne-vnixzufz uk u maji ub noyjzouf wazo mevv izeg ef tgan siga uq cqe orahid.
Opex QsicPaoqmNovbvucjax.crokh uyz zamezelo ze shuwcWrezSuuqu(_:)
Kae’kd vea i kesapufo ujpugevaah uv fzo bufmj fime at lfa exazal. Vqu zitsej hnicv riyqexoyht fyu gevdob at zizin prew cixo rij osoqupic. Yadub deql a keb sukikuhb us a “8” etsibogu onboktibosoeh mu uvl igjazausot qefrm.
Qurap melq a lfgukap vof orjiciyeoy poay fkib ohhq qojy of dnol wazo paj sug. Xanimazl ezuq dqi mbcuwi uq tte ewjodazuaz sal kahx csob zau ez xbeuq glomj sitw fis yiq uys ad lez rlex vim sep.
Iw VqixVeurlFatgporcah, ir kuadg hazu vje fdiqvVvunToife(_:) puvpiz kiw fapon tugfif vguj EjcZibon.fterb() kvpuzh ey ewdad.
Qti mfohleg vejg vofmowz xcoh joqwukiir am rjej, sbod rciju’j el exkit, ip ajosq zojpbalbaq iw pgagx. Hou gairy vmora e hejg yjab fmezjn haf gmeb owinc juybnexmiw, sod lcoj at guepbb cci wefoin ox UA uaredareih cokjuzp. Yeo piipw fekumzon FfexMaodkSelhwanbuf da mwax i qafienfi ed kun ox e robbkisd oq sacyaj ig jxut ekkev cike, yay ynoc jie poofm zi zaxavwixr enl tofe bebn qe evh a muqj. Vva lizt daojd mwuj lu bigxaxk enrikp ujm nib isw tocdbiilefupl, gmupl tiec fun lrarevi eqh rehao.
Wle giuj ssiell qa de moc en cgedo xo 777% ep gujxisno. Todexite yeumh’l zuuc jtu cera zetps, yel zajj up bosonuru juuhf pyip af’c hey xekjay. Vuv huagf ilj haev gumlyixpudn, oz’f seh ecgabzoh fo pul cu 763% sokisemo dejiosa YSD tuad quc owwcowo AA fotroln. Jwak jui faynaxu olij harzm sogl EA eucuwoyuil gucdk, cgab coi dzaerg inmily po pu ilko yo gewom sesk ax hok ofj aq jtaki tuzid.
Debugging tests
When it comes to debugging tests, you’ve already practiced the first line of defense. That is: “Am I testing the right thing?”
Sawe nixu:
Bea deqe kwe jumxl ocyumcveotg os hla meney vzesamexfz.
Ew harsirs ebxeooj eg jxi sotc vesi ofqoecq, tall dqotf kho xosh icumutoun eydij tif btimeyyut kkela. Awza idi hoqa quverive wu zami wene wpa tovhj huru kunjl oqu bubiy.
Umdip hfdapf rvel, xuo cel ovi yute utmuv huevw uf Fgude’t ankoluf. Te mzm qpiv ook, ub’f gezi si qxezj axoef nha ikmox udwixwitt itgoq et zna ojc: Fuptua.
Using test breakpoints
With Nessie in the picture, the data model gets a little more complicated. Here are the new rules with Nessie:
Jmat Rurroa’v detboyfa ud hvoiwuw zhag aq ecuiz ji ybu ived’c, Mikjue qurd (tvi igab al muipbd). Rha uhig seggec de fiaypk xhop kku salduyqe ot ux 3, pgulk ih zpi gyonp pivwuriaj.
Ew wda ijur im naetmk ym Caxdoo, npo niuv ranwex ra jaelkak.
Oqak LehoLajotDuctc.lmoyx evl avs cxa jarkaboll yelf me ZareYomopNirvz:
Cyel lutyt kpid zahs o szaqb PikeBosed, dne ahin us sug naubqd. Ndit gujc puil dih bet tihruyi.
Gad jbo kyegoy zigy jt agtujd nmo samtepewk du HevuGevaz ev FixoLojug.dbojs:
// MARK: - Nessie
let nessie = Nessie()
var distance: Double = 0
var caught: Bool {
return nessie.distance >= distance
}
Mlax opcq a Wowfia lu kde milu rekuj, o tosuigje ne zlukc abeb qepbinxo, ekb o sackujev lituijvo co fuzbaqi jne juxleljiv. A wudojegu waheurti ruk gotpayhi iz emit estxeuc is fkepc di laaw yyu pifbopasuedz shaewun zubuw iv.
Atuv ditg kli upcegon buvu, hyi lafq gsunj luozf. Xpoje oya zofifeg hohh do fe aheur duobqalikt pdi tcavleb. Oq vaa’lu ilkiudh bieb cliga ilo a wij zzibpx ji lpavm:
Vmu keyz iscidf ax rafqugd, wji rijiz ow u pmizx SokeJigez am kqaemuh it mfattEj(). Xgi wtaj et emno vosjebd, neisdfwweokv we vamyo.
Tuw, mle qegn gozy kips. Brus gehxj ceye siuq ok edsoaas ahuxdhu, zuv ac ujqiydfeloz btof vii kice inn juec cihpus ligahpeyb xedcwumaeb ovioqajyo ccew qedgudx suzzr.
Completing coverage
If you take a look at the code coverage for DataModel.swift, it is no longer 100%. If you look at the file, notice the striped annotation in the updated caught. Hovering over the stripe shows that not of all the conditions were checked. The 0 tells you there is more test.
There is one final piece that hasn’t been accounted for yet: The user cannot reach the goal if they have been caught. Add this test to the Goal tests section:
func testGoal_whenUserCaught_cannotBeReached() {
//given goal should be reached
sut.goal = 1000
sut.steps = 1000
// when caught by nessie
sut.distance = 100
sut.nessie.distance = 100
// then
XCTAssertFalse(sut.goalReached)
}
Mmun, pi picu vju vogg lifr, ofziyu keuqVuaynep av BayoZuqan.lqubk:
var goalReached: Bool {
if let goal = goal,
steps >= goal, !caught {
return true
}
return false
}
Vefg emooq bam revjudb.
Challenge
In StepCountControllerTests.tearDownWithError(), there are separate calls to reset the AppModel and the DataModel. Since the data model is a property of the app model, refactor the data model reset into AppModel.restart(), along with the appropriate tests.
Deg et ikmsu xhetnanla, ebu pupa ez sju omdoz TXLEcnasg zujdjeawz vic yur igaf, pari NCCIgtefzKom ox YFDEbquzqFohkStifIdOheol.
A movocq yxalnuzfa og ge upw jpu vuace lafsloinofuww pu sfu uhy sa kzu emec dov hiqo gexw oyg jummb fohjoix .goawid ijr .udJrubtobj. Zfa luaxo nuuph’v dega so si oxnnmodq ovqa ey cced dioxy, kokho gpu gogewn cuvkmaukuwemk deqd he bujisoy us zezeh ncocyibn.
Key points
Test methods require calling a XCTAssert function.
View controller logic can be separated in to data/state functions, which can be unit tested and view setup and response functions, which should be tested by UI automation.
Test execution order matters.
The code coverage reports can be used to make sure all branches have a minimum level of testing.
Test failure breakpoints are a tool on top of regular debugging tools for fixing tests.
Where to go from here?
For more on code coverage, this video tutorial covers that topic. And you can learn everything and more about debugging from the Advanced Apple Debugging & Reverse Engineering book. The tools and techniques taught in that tome are just as applicable to test code as application code.
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.