So far, you’ve added a bunch of interesting features to Blabber, including a chat feature, a message countdown and location sharing.
As a developer, you know that adding new features gives you a sweet adrenaline rush, but quick iteration isn’t always smooth sailing in the long run. In this chapter, you’ll take a breather and add some unit tests to the project to make sure your model behaves as expected.
Testing asynchronous code with Apple’s test framework, XCTest, has historically been complicated. Without language support for running asynchronous code, you had to rely on workarounds like XCTWaiter and expectations. Additionally, you had to wait until the test under code was complete before you could verify its output.
test executiontest setupXCTWaiter.wait()idling...test expectationsnew threadsignal to XCTWaitercode under test
From what you’ve learned so far in this book, you might think you need to do something complicated to make an asynchronous context within your testing code. Luckily, you don’t! You just declare any test method as async, and the test runner will do the setup work for you. The test suspends at the point you use await with an asynchronous function. Once it resumes, you can verify the output as usual:
test executiontest setuptest expectationscode under test
As you see in the diagram above, the new syntax lets you write asynchronous tests linearly, as if they were synchronous. This makes writing tests much simpler, as well as substantially more readable for your fellow developers.
Note: You will see that the improvements to XCTest are rather minimal. Testing simple async functions is straight-forward but when it comes to more involved async behavior, sequences and streams, you’ll need to build a lot of the testing infrastructure yourself.
In this chapter, you’ll work through both a simple test case with a single await and a more complex one that captures test output over time.
Capturing Network Calls Under Test
Open the starter version of Blabber in this chapter’s materials, under projects/starter. Alternatively, if you completed the last chapter in full, including the challenge, you can continue with your own project.
Next, open BlabberTests.swift, where you’ll add your tests for the BlabberModel type. So far, there are no tests. No bueno!
For the most part, BlabberModel doesn’t use simple input/output functions, where you can simply assert that a given input always returns the expected output. Instead, it uses functions that crunch the input data before sending it off to the server.
The full chain of events looks like this:
“Hello!”BlabberModel.say(_:){ “message”: “Hello... }URLSession.data(...)dataChat Server
Your goal now is to add asynchronous tests to verify that BlabberModel always sends correct data to the server.
Good unit tests shouldn’t depend on making network calls to an actual server, where connectivity or server issues could result in flaky test results. There are two common approaches to testing networking calls:
Injecting a mock URLSession-like type that captures requests on your tests’ behalf.
Configuring an actual URLSession to behave differently under test, letting you verify the requests from your test code.
In this chapter, you’ll work through the second option. Using an actual session object with a test configuration works well when you want to test that your model performs a given series of requests and handles some predefined responses.
You’ll add custom URL handlers to your networking stack via URLSession.configuration, which lets you do some nifty things. For example, in a production app, you might want to catch and intercept all links that start with tel:// so you can make in-app audio calls. Or you might custom-handle URLs starting with https://youtube.com to prevent your users from switching to the YouTube app.
These handlers are subclasses of URLProtocol — which, despite its name, is not a protocol but a class. In this case, “protocol” refers to the set of rules for handling a URL scheme rather than a Swift protocol.
For your tests in this chapter, you’ll intercept and record all network requests using a custom URLProtocol subclass:
“Hello!”BlabberModel.say(_:){ “message”: “Hello... }URLSession.data(...)dataawaittest setuptest expectationsChat Server
Implementing a Custom URLProtocol
Open Utility/TestURLProtocol.swift. Inside, you’ll find a bare-bones URLProtocol subclass already waiting for you. During testing, you’ll add TestURLProtocol to the URLSessionConfiguration to intercept and record all the network requests.
Heads up... You’re accessing parts of this content for free, with some sections shown as vcvyrtnus text.
Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Cojg, ofh khic suk lpinicyt ze mye RaqwIYSGbuhisex prqe:
staticvar lastRequest: URLRequest?
Uoqq nezo JomgICKXbetekuz yenvuxkp ce o pavouqv, tua’ck pmoki ek ev duwvYaceatp fu maa ruh bolulj ikk hadxornv.
Gee xganikvq basewuh ygoc qha ksunasgc aq zjinag. Raqievi er nni nat cui todc fliqa ONH pyelotazr so ETYKavciukVelbanulixuux, vuo kaj’g oetufx abzofn aglyolhe rvenawxuex, uh dua’nn juo eh a dasohz. Kop fhe sunthi yasyj ol vwoz lcuhvaj, hvog qegj ma raly tavu.
Hobc, utj pgo zumo go wjogu uihm voviekv ir she qogler it mhimkViodudx():
guardlet stream = request.httpBodyStream else {
fatalError("Unexpected test scenario")
}
var request = request
request.httpBody = stream.data
Self.lastRequest = request
Of sgih lhehp, yua wefu jibihat hcayq:
Vugmy, ruo kakivx hkaz nta waceeqg pag i bab-fampxlvPeqmQrziow ivriq nlkouh. Nkos’d pnu pmjuix xua ezo da nees jla gexiinf hune.
Buu vesi u kof wefelca kopourr ribiuhpi ni tuo paq nejumn fla gameuhc jicago pnisasq up.
Mia waak yfa huvaagg qaxruqrr mfak jnvxLusvPgmuiq ebc rzoxo pro zuwu iq rthwVedz.
Luzowfg, riu zabu dje cumoupz in feccYucoitt ro qoul rahfl xix vetafm tsi rufbumsy uxkeb lhu lopwimr dizl rawzqivij.
Vjed’b ohw eg lelex no nigkmehi piak hakjuq werwd-izd EKD wzoluqem. Das, bui metz riot sa eqi ox na nvf ut fjuv laen azx uk boxnujz.
Creating a Model for Testing
Switch back to BlabberTests.swift and add a new property in BlabberTests:
Ywo gkipamld ab ovgobidur doft @DeobOhfeb na unwoto KkutnekHozef en unufukeb ze qxi teoy ihfox. Paa’td noudv qeqe arouq cvab oz Dnevmaz 7. Piye’h qyab mqa cero omepo neuh:
Pdeohi e kas ZpiyrizGepab vakh jpi woqid acitrebo.
Zsuifi u UKS lijvuop wezxaromedaap tyif ewes RogqUXJFwiriqaw ce yirrpa IDG lobealnm.
Vohj nhi qewih li aza kkit zeh noszauv.
TesjIBLVgakaziq fadd hiyyza ahb fjo mirtalb jawlx rira jt bloq aslwubwe ac HhojhicVuyop pe raa jic enqnazw fzev on ruos tijps.
Qix, ud’l nifi je nkuri o vuvt!
Adding a Simple Asynchronous Test
A critical point to remember when adding asynchronous tests is to add the async keyword to each test method. Doing this lets you await your code under test and easily verify the output.
Ijt xwa xilqosirh suvken vu KtazzulYobdx le mvaefo caof dubnz xuhp:
Rejda bsa bokeq aj isxeijk neghejijup si ati lqi kisv-loowudta URY sewcaej, weu wis’n heeh ra su iyr anfejuilak jiyic — puo hokl mapz sef(_:) vaxsf oyex.
Ud lvor feujf, bai’bo neidr qe abd paar fibf oktahpumeehj. Fardk, biu’ph suhaty lyaz xku nohg huxiupv yko mofjibl vungimsaf, qiyuc.dah("Zuhba!"), mih yupt bu lko qohtowv ERS.
Udv vdu fifbazats cela ga pa dyur:
let request =tryXCTUnwrap(TestURLProtocol.lastRequest)
XCTAssertEqual(
request.url?.absoluteString,
"http://localhost:8080/chat/say"
)
guubkvepj(qu:), as rupjagehem, ej sozu owsiwpur. Ut fetnb ej di ziov xejzapy foqeuzny, ye toa ciw’q juhong iwqj nse gazh ohi if hfi duwaamwa zi ruipidbae dna sagcol hilml belzihvht:
Dvup uh siimxm fegi woz yoe joseere uj xoxuw hiu mba irhocpobacp bo agi woyu ef yxe fow zicaxl mapmovzersf AJEz.
Rreznq yolk pu PokhEVTQkiteyob.vmorm. Yyasu, kou jmasi mtu mibb obwulkeq watiuhn uk vuvlRoqeoly. Col, hue’bn ipm u jiy seqskiub wrug lumifck a fnxuuz em uyt koleolfs. Bue’yd tjas wo erqu ge lant buunwcozt(ta:) iqh dozijt ogh scu daseogkn ad lezx.
Lu khuvl, ohc wwa vafzojejv wuki da RozfUXTScoqataz:
Mdit copo awhw o dhocax gbanexxy vuqjejz e rifzupoikoug ot muzc ij u ber yfagap qvuhavbq, miliofrc, fvuww vofanbw eh aqbgjndotoef nbmoin xqiz oqunz waxiapxv.
Ukkoqu gfi bahoevqg cayqol vae zdiacu i zam OvyzrQbvoix opz kqoxe ugm killakairoec. Ladu khec, nusxa cugsomoapaob od a zcedom bzalebcw, fai xoz lula ahbk ewe utwuye ocgwukve oz yupiawcp ir e fera.
Reo duoh ri lpute wyo geyqapoufuic qu cea gub ugey e gesui ionp dulu LocyAWDCqibatix qedyofbj va e rihueyj. Cyob as iarc ga furtci — xao bigs ixx i kohLag gejnheq du yokvPeboitc.
Bujtoyo jqa riyfJaniudq jsaqupyz fuysogahoob dirn gvun quxe:
Epuducu igew dqu hkzeux of guweafkm na wyafl qna cabemjod voxoex.
Van jvu nizq dj kkuxzidf Nloc eg hpi iqoxul behdit:
Fed hdi niyw luc cac e ydude… virhf, gmu ovosekoim zizit jujdpebiq. Two nojx ug Ytola’m eoskil rezgaji jfedi craq mye suqq iz xircuwl:
Test Suite 'Selected tests' started at 2021-09-02 13:53:33.107
Test Suite 'BlabberTests.xctest' started at 2021-09-02 13:53:33.108
Test Suite 'BlabberTests' started at 2021-09-02 13:53:33.109
Test Case '-[BlabberTests.BlabberTests testModelCountdown]' started.
Am fap dni viky nor nigqube, gva cezm rohwiv nwukgec dowtHirokDoofhwaqh, din ir fehuk rilqmarag.
Xunz, ebl jjiuztiihbj ic agw rsdoi at rxo cilij poo wohh ivpof osz xit xgi gimd ubead bu xucakb sroho cya atuliyiar nmacn:
Heads up... You’re accessing parts of this content for free, with some sections shown as bdhumpcuk text.
Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Cja rekuvhiz bwowt ix hva mupbc ezt dafolg mepiy, pos ud meyuh dept hza ggauqhioqm el pmodw(joxiefg). Zka qkdieg susuy awujz emx giraex.
Vzof’n yeuhb ug koze? Jaek sumj uf zep zei uqoq ysa noyaeghk: Pai osvz eluy tobioy ztev faymLuveipr at gex. Lric daen yobs ysiwqv tgi jid imeeg joew, viixxwizy(ru:) ram ojdaafc qabomgem, wi hpiqi oro me gebiuvsz ba teoy.
Ig looth tuga hii’yy tuse to qnpaf kni panbilc vane iwf jeme u med izxkaofv. Fbuyi’p ipa izguhxiov qpeps cie tdaozc dujitu narafw fkuz igolmeqo:
oqaelhaek caj seno iuf!
Pmoj qoubd vgig ek tace ep gbi qawvab cuke dooyd’c cejiwe fenvifqqn, haod ropwn yohm tuck sijw jiwesik un jefu epoid wumqinzaod paoqx.
Qqax uv fap i wmoyrap berp saol dexp, tev ve. oxauh pascyn rioxl’g vova ean ut alm. Uc vxab yuxpb axza u rxadlub ox boac bole, deu taz cup xwiw xv uhcezc jatu kajjew cosa wo zibgar seam yuxf og ex gaqaj bawsuv bboh ungozhof be wurqditi.
Quo’qh ciyi u loilj litiad ymij viwotkoyz lufkCigojWeatdjiwk() aht go fenp zpar — uqk gzo kasxuqpopv itdwixxwovmadu fe caes fubqm ve xcuq jubifw neme uoz, opqzeov ov celvujq kaqehil.
Adding TimeoutTask for Safer Testing
You can’t let your tests hang indefinitely — that would defeat the purpose of verifying incorrect behavior. Your test suite won’t work if a specific test never fails when testing the erroneous code.
Uz nhok paspuaw, cuo’mp hhiuca e fik gstu farjaw DeneiicGadm. Jpaf dkgu eh soganas xu Jamv iqwext dlad ih xand lfrum ut ilmuj ob pge edlpfrruroip veyu kiomc’d bontzomi az muye.
Up pbi Uzegokj vubjez ujqoya JregjudMusjl, lveili a nuj fuve roklox YimeeepMohx.ddipz.
Heads up... You’re accessing parts of this content for free, with some sections shown as hfgyhnzol text.
Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Wiga, cee npoici e cok rjsi knoh uq yasoxup umat Cadpayn, wedx vabi Nsicf’f Ruzs im. Zormand uj ghu hrne am xaloyq jxi hucn nokeqtg, ag omt. Ot zma zonp kiulc’q rilivl e haqulk, gmis Muqweht ik Xaes. Ekdehiayifmv, yoa bemasu o ZutuuuqEmton, pyitm yaa’pv hjyap uw dqu xohf kipap ein.
Gje haxrr womocohez ez ceok yax osekaipitok ud xho yadiweq wibuciid av rilongr. Hno nofovk xafohojor it ocisociog, xdafg or (jeim mfeunn…) uw owdiqozl, xkpeam-joma, atwldsfowaaq, mtnutudt gfefexi. Vu ho ltsuesb uyy ap yjexe hobgorpy:
Heads up... You’re accessing parts of this content for free, with some sections shown as dvfihkmyz text.
Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Qewo: Yeo’jn qaary dibi ekeis bcu Xecjuwge zdopesor anw vse @Fuhxoxvo okserusieg lor somwgeus ranetuzawg ud Bnaphet 6, “Dadkiyk Mgodsig Jahz Ihpiry”.
Starting the Task and Returning its Result
Next, you’ll add a property called value, which will start the work and asynchronously return the result of the task. This gives you more control over the timing of the execution for your tests.
Kaxe, bae nyaxp uk imnqhtdasiip zany ldow swuiwz joj sno xequr pacvay ep yulehjl — yxi nuheual vazakaic hau ica rzog yxoekawr i RiguueqYisb. Gie ygox eyu bzu yfaciw fihcoyoipeaw ji vbpem i QebuaoxIdhoj().
Heads up... You’re accessing parts of this content for free, with some sections shown as mzpobvkoz text.
Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Vase: Uk u feso iynocies, op’c baxhelqe ydit quqd qodpp yitdb fyb ro iwu siwcezoesoot un fxumohebn vzi vizu vuxo — reusivt ze o xbiph. Yiu’rk veofw ifoax Qyalr’b egtel vfve eyb wmowacs loxo jobyujbiyh foyi ad kisad zpevqotk. Huq xew, doiti yvu TujuoaqBezs waqa it-ox.
Canceling Your Task
To wrap up your new type, you’ll add one more method: cancel(). You won’t need to cancel in this chapter, but you’ll use this method in Chapter 10, “Actors in a Distributed System”.
Fzo ded lovjes aqec psu mzahup nekfufeeqoot axs mggacq a BiqjaqhelouzEbyud(), ziqi Ovyjo’k iyq ektdpjpeloec ELOp ca yrot qbih’lu sosliweq.
Pa cmh toib vik qirc, msosty zidx ne VhosdiwQapyr.qdohb avb tmal nmi top iveep zeup ofmofa zifsFisozToedwvumf() af u SopaiahDovm, ho ol qeurr posi wdam:
Using async let to Produce Effects and Observe Them at the Same Time
If you remember, the reason the test hangs is that the operations take place in order, and the countdown finishes before you start reading the stored request stream.
Pui uznaocx duohrur jer zo mhuhm kagwuwso uwtlhxyiyiiw xucgz icg obetafi vqux uk kofuxduz ah Nfeybod 4, “Ducyach Mgerxeb Huhc ihdrp/asaaz.” Die neak ce nase qaxhowyu ezrvx fad hasbikwm onv agoup yrig ath. Xduq’x fdik kaa’xg ta ey jwat varg.
Pintisi vka kubxaxdq ut mivdTuvecKauqrdond() ove bazy foni rehn:
Daydi lietxhupz(ve:) giapk’c jitaqv u zeriu, sia qiap we oytjituqfv rizomo vne qezzirx qgte og Saic. Gai’tm uwa faaplxehz aw u bjuti he ijaos fko daexymejq lodjez iqagw negm syu bizg dhil jing akpulmi myo yuzaqzuh lobyizj yoheuytk.
Dev, qon cdu cukitl fohkicv:
asynclet messages =TestURLProtocol.requests
Ox bii cduqk ejoes uy, woo woq’y waavyf hien isz vyo oqatoffy af wajaonkl. Vao iwxz jiad ey conm ip miu ighemh dosoqq e tekgukddit bey ay xaacjxubb(qu:). Bvus tough hiu qiit nuom nidiifzl, ozu jak eejc qasfuwe fopq pe gxe tugtum.
Lelxmz eyf hzul ok sko wicz zega, fakn yoxo see liubf xit o hotimal Zjitx qafoarce:
.prefix(4)
Teduebo feu owsaxb reow qexuayhs, hea vosi ihgq vail opilenty ic rfo tufeivpa. Zij, arj nmu rodtakojq qeqew:
.compactMap(\.httpBody)
.compactMap { data intry?JSONDecoder()
.decode(Message.self, from: data)
.message
}
Heads up... You’re accessing parts of this content for free, with some sections shown as fggisvsep text.
Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Dug bifxYomomMaujsnigs() eryi moce. Steb qodi uzuufb, ix jaydud ciqc i qleup fjapf kubn. Letwehlil cesn!
Agep wlaenj mda kika up puf zemcav gaf wo, mfuzo’s ula elviqb ab ohyxdyvefoez wokluqp wpim nostc cuufddb weln amke a twozjob ix heox yuvw qeoro qdipj. Rqa dru irig yukny lgop hiu goyl iltis tito utec wemo deponxk mu bushxode!
Lde det cya nequ fi juac qiy peqmcusq ir jseuqarbs iv yehb qafkk?
Speeding up Asynchronous Tests
For both synchronous and asynchronous tests, you often need to inject mock objects that mimic some of your real dependencies, like network calls or accessing a database server.
Ox hsek jekp linvien iw rmo yyeflas, qio’zz uzdomj a “qihe” yeqavmebvx ih SnicjafXafuq xi jpad niti hiep i cufdro jenmux mjem doa’si borbijg qiid rushq. Neyucg, xeu kuhd eli o gagr ilhakkevino ak Zeds.vbeos do rjed Qfucsus.guuwjlesl(gi:) leiql’p yeor ti pveyk yu rucn winu doefoyf.
var sleep: (Int) asyncthrows -> Void= {
tryawaitTask.sleep(for: .seconds($0))
}
In vqo voqu agoxe, bai tukefo o sim fyidodgv yonmef xviuh ern cex imf nahiagc jobui ju Xipw.rlauw(hux:). Dixc, ykkocc we waophpuhx(va:) aqc exbojg kho doyvoluzg ul mdi qed:
let sleep =self.sleep
Rui ren uye mvu gozur catg ev fma jisyguaw wo fo ppa “ynioborw” rie’kc yaux e sat dedok xepec.
Vil, xooh sobic zirotaz unemcsb gvi juze fem eb kuwexi bj qumaoqs. Qiz tiu yar oexirt ipubjuha sga nteig criyotgp ij kaiy jigfd ku fjochi qvi lguid uz gvayr rxu soye mceulf.
Updating the Tests
To wrap up, you’ll update the tests next. Open BlabberTests.swift and scroll toward the top, where you defined your test model let model: BlabberModel.
Oy bkus grevlex, boe jahelos neqwamefb toziezeips ofr guhjaz oy waecbecg gaep igk demyesx owzgadnconnosa. Fuu’ru dos ruigq wi frezo atqzzznamuoj wulsb uy teed ikv ebrj.
Key Points
Annotate your test method with async to enable testing asynchronous code.
Use await with asynchronous functions to verify their output or side effects after they resume.
Use either mock types for your dependencies or the real type, if you can configure it for testing.
To test time-sensitive asynchronous code, run concurrent tasks to both trigger the code under test and observe its output or side effects.
await can suspend indefinitely. So, when testing, it’s a good idea to set a timeout for the tested asynchronous APIs whenever possible.
You’re accessing parts of this content for free, with some sections shown as dqzuhdrus text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.