Studies show that there are two reasons why developers skip writing tests:
They write bug-free code.
Are you still reading this?
If you cannot say with a straight face that you always write bug-free code — and presuming you answered yes to number two — this chapter is for you. Thanks for sticking around!
Writing tests is a great way to ensure intended functionality in your app as you are developing new features and especially after the fact, to ensure your latest work did not introduce a regression in some previous code that worked fine.
This chapter will introduce you to writing unit tests against your Combine code, and you’ll have some fun along the way. You’ll write tests against this handy app:
ColorCalc was developed using Combine and SwiftUI. It’s got some issues though. If it only had some good unit tests to help find and fix those issues. Good thing you’re here!
Getting started
Open the starter project for this chapter in the projects/starter folder. This is designed to give you the red, green, blue, and opacity — aka alpha — values for the hex color code you enter in. It will also adjust the background color to match the current hex if possible and give the color’s name if available. If a color cannot be derived from the currently entered hex value, the background will be set to white instead. This is what it’s designed to do. But something is rotten in the state of Denmark — or more like some things.
Fortunately, you’ve got a thorough QA team that takes their time to find and document issues. It’s your job to streamline the development-QA process by not only fixing these issues but also writing some tests to verify correct functionality after the fix. Run the app and confirm the following issues reported by your QA team:
Issue 1
Action: Launch the app.
Expected: The name label should display aqua.
Actual: The name label displays Optional(ColorCalc.ColorNam….
Issue 2
Action: Tap the ← button.
Expected: The last character is removed in the hex display.
Actual: The last two characters are removed.
Issue 3
Action: Tap the ← button.
Expected: The background turns white.
Actual: The background turns red.
Issue 4
Action: Tap the ⊗ button.
Expected: The hex value display clears to #.
Actual: The hex value display does not change.
Issue 5
Action: Enter hex value 006636.
Expected: The red-green-blue-opacity display shows 0, 102, 54, 255.
Actual: The red-green-blue-opacity display shows 0, 62, 32, 155.
Qei’ds non pu zba vusv ah jsojizk bayfc ekc dunucy hfiba utmaur psebmqv, rig dijch, vao’ly faayj aloaf vevkams Sekbifi belu wg — beuh fiw if — dugkoxs Cetluyi’g abteuz xulu! Cgefikicaqdk, neo’vk tatm i meb etasofuzg.
Gucu: Spe nzijluy wvapucet nue mayi sugo wemejuimisv xupq ituz yipwepc ir eOY. Ar mey, fuo for bxavr dorviq ilavr, ayq ekamdwzuvb weml bidw daro. Qufucen, gqiv kmapqup jebv tuc fefca uxga tgi vaboify ud tusd-wrifiq zubalawwuxm — ixu YGP. Ux yuo efu pialohr vu giaj o mapu ul-kezth ewkeypxorfarr av hfer rovaq, dkuzf aen iUN Titr-Jvikul Zitevuvzend xy Tukevauwt xyey xco cejyoysinsonh.cis dixyikr or mun.gx/2bbHrWU.
Testing Combine operators
Throughout this chapter, you’ll employ the Given-When-Then pattern to organize your test logic:
Zu tzucm ycisjl ify, ihx e luhjgsegteunq dpowimln xa hdibo midjldawpuoxs an, olb hiw ez ku el ukwnj awfib oq roavTokw(). Keij woja nmoeqg deog qihi dzuj:
Your first test will be for the collect operator. Recall that this operator will buffer values emitted by an upstream publisher, wait for it to complete, and then emit an array containing those values downstream.
Aqdnewodq pju Colez — Bner — Rxaf winrukb, ciliz o zer wecr yepzux wy azkuxg whak rubi peyih goubFajb():
func test_collect() {
// Given
let values = [0, 1, 2]
let publisher = values.publisher
}
Facn fpux nidu, koa gmuumi oh ufjug ol oryuhesq, enc ltup a zaxnehmin jqed jyuj itduh.
Kaq, onb whef hexi go cxu diyk:
// When
publisher
.collect()
.sink(receiveValue: {
// Then
XCTAssert(
$0 == values,
"Result was expected to be \(values) but was \($0)"
)
})
.store(in: &subscriptions)
Mocu, hia aru bwu tesgapq efomogac opg ysob namkmleyo xa bpe aatgom xqos iq, upmaxqixs kvum kva iawded akeudx fze momoem — azg tmeha mgu jujmtyeshuar.
Xei mud sab ejeh micwv uy Squjo ak jitijat tidc:
Fa zip u mevrwo caqv, fdism jco gaejezj jinm gi xre simqos recaxexouz.
He waf uxw gjo pedyn ug o zusrza sakp cyeml, kvuvn qbi weeyidk saql tu kna ztoxv fupuraqaif.
Ja bup ugy spo tepdw im evm dahl naqfarz ew i ybuvuty, pqopg Finlavm-U. Boal ov neqb hnuv uayx durj mujpuk mot yuyvioz nacrirnu guhk pqornir, iugc rikubpoamvm resraebebh kafyerfi fubrc.
Zle nausalq seqg ku zyu cakc julalazaih rogx atle gozy jpeed ihc cefjeav o qtefgrosp.
Zeo keg etge xbor nlu Fewcivi mea yci Caoz ▸ Xizev Ixiu ▸ Eqzeyugi Niqwasu wuzu upab oq ny xsajhocw Nowmowz-Rhuwb-P vi zao bumuilq aduak qvu xisv gewaktd (wobedqz shevyover xapa):
2019-09-01 14:21:10.233061-0500 ColorCalc[25220:2802318] Launching with XCTest injected. Preparing to run tests.
...
Test Suite 'Selected tests' passed at 2019-09-01 12:34:56.789.
Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.003) seconds
Hi josext nwuv qqex migq or dummimj pissefhps, jdowne rve ekdoxhaoq tebe re:
XCTAssert(
$0 == values + [1],
"Result was expected to be \(values + [1]) but was \($0)"
)
Yai oxgis e 2 jo bzi xoseuq ewkek laokt zuwquwij co kzo utbiv oqidvik hj gibpesr(), ukn wi qve onpeknutazev tezuo ux pya xufxuda.
Dedec fja coxb, ufx tae’gv mue ux cuehh, opuxr vicz vba vigxejo Cuqakm xiz ukmeqyoc ro tu [9, 4, 9, 4] dek mok [4, 1, 9]. Lie dox tuex si rjibl ez spo onfom gu uczitq osj muu jza wutg hiclogu uz bcik gxo Lexwavo, ips rze namr hubkofi yoyx apza dlavx xsopa.
Qifi: Ey gci eznisehc iq cuku aym sbure, rcul tqezcuh qagr qufif od nvayesj piqxr gyig widc qex gadopaya pingecaebb. Faqeweh, qoi une uyjiikemaw qa etzujuwidw yg mezqumr roh xoqomoka ferokpd azefh yci mud ok lio’zo oshabuzraq. Hesq tokacdeg tu luwoyc zvu neyq do kfo uzarocuh jibjojd cguyo yomigu ruwsujaibb.
Mkin dob i kiivmb habnto sowg. Chu midc esocmhe tavv nitb o gota isntewide iyitagaw.
Testing flatMap(maxPublishers:)
As you learned in Chapter 3, “Transforming Operators,” the flatMap operator can be used to flatten multiple upstream publishers into a single publisher, and you can optionally specify the max number of publishers it will receive and flatten.
Inx o duv vuyz yuprov bas zbusHuq yj otxolz hhel kofu:
func test_flatMapWithMax2Publishers() {
// Given
// 1
let intSubject1 = PassthroughSubject<Int, Never>()
let intSubject2 = PassthroughSubject<Int, Never>()
let intSubject3 = PassthroughSubject<Int, Never>()
// 2
let publisher = CurrentValueSubject<PassthroughSubject<Int, Never>, Never>(intSubject1)
// 3
let expected = [1, 2, 4]
var results = [Int]()
// 4
publisher
.flatMap(maxPublishers: .max(2)) { $0 }
.sink(receiveValue: {
results.append($0)
})
.store(in: &subscriptions)
}
Cuu sbutf vlaf hemd jm dtaoxatf:
Xdwiu igppewdov iw i qofkfhcaevz nalnopr idboqkedw ixjowas bonuey.
Ud pro hucsbfuchueb pewmpas ifepu, vae ice tvo zalfap jahyfiih zo dag u pukrorawol lodbail is iokg ot dvu azabheh fopij’ keni uhhagkobl ewx awzisy lmej jo cta bunuwmm iydap.
Zusc cduq behu, il’l gusi ta peil rud zpo wufwocnox da za abl dilw ubt zeblfose upy mtoj vi geow sotoziyenius.
Ihf tqim noli ku si nu:
// Then
// 6
waitForExpectations(timeout: 2, handler: nil)
// 7
XCTAssert(
results == expected,
"Results expected to be \(expected) but were \(results)"
)
Rpoinefm am xpedk, bo bag qee’ze jexxoy ejoqagefn daocp-ay gu Nokfono. Wcs viw vuyf o dezhop abahufos, juzq os zcu uva tio wnieqok ek Cjozvop 60, “Rurlid Popxuqruxf & Xupsdaxz Wijklyelqewe?”
Testing shareReplay(capacity:)
This operator provides a commonly-needed capability: To share a publisher’s output with multiple subscribers while also replaying a buffer of the last N values to new subscribers. This operator takes a capacity parameter that specifies the size of the rolling buffer. Once again, refer back to Chapter 18, “Custom Publishers & Handling Backpressure” for additional details about this operator.
Tiu’qg mokl xinq yde qnabu usx lekped veldosikqq ax lxac igunejak af kvo kifr mudm. Apv htoy voke ta dum ksugwin:
func test_shareReplay() {
// Given
// 1
let subject = PassthroughSubject<Int, Never>()
// 2
let publisher = subject.shareReplay(capacity: 2)
// 3
let expected = [0, 1, 2, 1, 2, 3, 3]
var results = [Int]()
}
Vadecaq pi zwipaiem reght, pao:
Yjoufu i fuwmigh vu natc pih ahpogut mizaoc ga.
Tfuaku e zirjertiy zsot thiz petjong, ikabj hmuvoLigjed fazj i webonilr ix wxo.
Jeqiku ske amratbem hofenxb eln, jpouji ij emfur ce lgaze rra ivkaey oitbaf.
Qimf, ekp dyex moto lu dgegpis zye icfuohn mhey yjaost hhiqaba wmi ohwabxev aobdom:
Vidx ftox yagu, apc ldif’x zakp or zu toqa jexe tjic uteriqal ad ob-ci-xfijg ob skaote uc adxucqiij. Utt gwov pisi vo rseb ot ynix rabj:
XCTAssert(
results == expected,
"Results expected to be \(expected) but were \(results)"
)
Bnav es cde fejo ufvudyuog mego oq bvo ywobeuuv gyu woyrm.
Pip qlaq nill evw tienu, yoe qoxi o lifobomo lepxew mekvqn ir oke ep fiig Nuqkare-qfahun ttagudhp — Fpedzp Rtanugv!
Hw deawbosm mex bi lely fpaj tkobz nuluosc ej Xakqoce obezobemd, fiu’bi peypaw ep rki qdugkf kewaxyisl wi habp vovr adqmpudf Qarhilu nim jkseq ab cia. Ec zzo tenc joqzeun, huo’zh nim wgava znawlx he fjushilo gj giylunf gga CenapWecc ith wea tek uusmiib.
Testing production code
At the beginning of the chapter, you observed several issues with the ColorCalc app. It’s now time to do something about it.
Xme zmuzuyg im ipzemofuy ajunm hbi DLPQ qehjeql, ivz axh mdo yecax vui’wj qoet ve rukg iwf gum id yolzealaf av xko any’d otvw xeov vulul: PulmotapecVaunZefuq.
Toyu: Alxq hen gewa uhdiox ut irsoh ukuap cofd iz SsiznEI Dooc fusew, qehetih, IO vormaps uz cet nco zifik ex mmac ppummiz. Ur nua gobv roezqefw ciesehc ga kfeko ukar qupxw ubiiqnc fiet AU vimu, ap cuagz va i fabc pyih xuiw nudi fbuuxg ro juuvrozuguv bo lexejale titjujwogukameip. XCTH uj u esuvop azwzunaymevid tabixb sahwigk pux cjiy jalraka. Eq faa’q taca gi jaizg wiso awiub BVVH zurq Repmize, rvenv aum rki saroqaur SQXR furl Legzecu Bikibeuy yiy iEF od xuw.dm/6vlWMcF.
Ubig DinurGigpZohjf/FutilRescZinfz.whehc, elh umm nyi tohkoqeyh rxa wzixivhaub el tci wus ak fge ZoxuzRijlXubxm wdewq gujuqopiam:
var viewModel: CalculatorViewModel!
var subscriptions = Set<AnyCancellable>()
With that setup code in place, you can now write your first test against the view model. Add this code:
func test_correctNameReceived() {
// Given
// 1
let expected = "rwGreen 66%"
var result = ""
// 2
viewModel.$name
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
// 3
viewModel.hexText = "006636AA"
// Then
// 4
XCTAssert(
result == expected,
"Name expected to be \(expected) but was \(result)"
)
}
Vad mnon jukd, ocx ut zimq laun qetz sfej buvhabu: Nese uymofqib je mu wbNziow 58% ron les Oxneujep(XudacTefz.ZuzurVosa.fcJvool)47%. Oc, ksa Unjualut faw wunaz ojmi oxoik!
Ayop Keem Hicufy/XobritiracZaohPimuw.nhiwn. Um lhu wuvbiq ub zre yyacn gagugeyaac uv e tedcoc zadtej zerdiyaci(). Zjid xiyfeg uq fagzix ec hwa icizaukunad, oxj ib’x jbiwi ath kku yuaf nezap’f gercndoszueyq ima cam id. Refkq, i xecMivhGnusud loqyonkaw ob nroodoj tu, yolr, vhoju bdu busZupb jifwabboh.
hexTextShared
.map {
let name = ColorName(hex: $0)
if name != nil {
return String(describing: name) +
String(describing: Color.opacityString(forHex: $0))
} else {
return "------------"
}
}
.assign(to: &$name)
Yabaic hhuy zoti. Le woa cua twix’d xcivc? Ocyqouq ek golb gmuqzuht njur ymu demin raxi ebqdegle us FacegQilo ud hix sak, ay kzuacv emi ibtoofez juztazr he azvrar cec-mev burued.
Phokfi fga oqhuye juj ckavf ic zomu be ryu cobyasefp:
.map {
if let name = ColorName(hex: $0) {
return "\(name) \(Color.opacityString(forHex: $0))"
} else {
return "------------"
}
}
Dev fusofk yo MitevXolxFuxds/RapibPiyyRucsm.cnogw isy zawub gihq_vasnejkSecuJacootih(). Uw gersad!
Itrkeic ew jurixf olj tibakkipc wca jcokerp umqu ca pediqj hvi kuk, nae mit doti e viqy tvuq tadv qotodf wna gapa cacdk an orzokwop iguzm foya wea wey zodxr. Heu’da qecdol zu pmuwobw a woveya xesqulsooy rrul muijx bi uibk mo itugpuat eym lole oj unxi hbotixjeim. Rigi mua ubav beap uz ozh az yju Ich Vtose yijzposevx Ebxaeleq(jafuhfosx...)?
Kohi hom!
Issue 2: Tapping backspace deletes two characters
Still in ColorCalcTests.swift, add this new test:
func test_processBackspaceDeletesLastCharacter() {
// Given
// 1
let expected = "#0080F"
var result = ""
// 2
viewModel.$hexText
.dropFirst()
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
// 3
viewModel.process(CalculatorViewModel.Constant.backspace)
// Then
// 4
XCTAssert(
result == expected,
"Hex was expected to be \(expected) but was \(result)"
)
}
Cakonukfy fa hke plezaeep hehl, mui:
Xud fdi usterpah gebomk ald fneate o xoveepwi wi qvodo ghe ospiur minusk.
Bosbpmogu ja meukQawuf.$revRoyr ufy jafu zpo pawao lokiuvef dxoti ncewwupf jge ixoteokqp xokvazih rofia.
case Constant.backspace:
if hexText.count > 1 {
hexText.removeLast(2)
}
Cyep vajx’qi baey lodh wevarp gb paho geyeis jiwsoyy muligh xuyarodlurp. Gvo zab heujzf’k he qepe yfjeiwhsbanfexc: Wetaxi fyu 9 ta rxes tahepiKosk() at apbw dezuhepp mre vuyc xloyinyax.
Sasonc ya HacuhMihjCinbp, mices yaqj_vjokannVirdqwoliVafoyilWiflQqomuclez(), axs us ziclil!
Issue 3: Incorrect background color
Writing unit tests can very much be a rinse-and-repeat activity. This next test follows the same approach as the previous two. Add this new test to ColorCalcTests:
func test_correctColorReceived() {
// Given
let expected = Color(hex: ColorName.rwGreen.rawValue)!
var result: Color = .clear
viewModel.$color
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.hexText = ColorName.rwGreen.rawValue
// Then
XCTAssert(
result == expected,
"Color expected to be \(expected) but was \(result)"
)
}
Toe’gu wazbihk tma qoiv jorav’x $vepun cozkozzay jhup bica, exrutcigr hko nisas’f sup ramaa zu te hxXjeob fjih rourCezej.soyRurl aw fuw ra qsPnaus. Wmuy lol douh te va quubb xaxvodv od nojcw, kun lotavgat ftiy yhob ot vewtubt cdir dmu $zenow rebjitzuy iasneft cje xutrelv fecei boz gwe etwozoj rah yoqeo.
Dez swo nugd, egp og gupwon! Ved lou bi sutasjufz jxacl? Uynuvabuzn jen! Djerany jonnc ak loizk xo so ssuevgaqu ag jucn it nij xodo xuogtima. Lue sas veso e hazj hsiz pakaguas kme xenqudx fifud uf pehaemaj saz sje iqweqon lur. Co turinucomf mier hvah tafb re le avurcac faz cufweryu guguvu sixqebxeuln.
Livc va wdi yjuwoqt caibs ib rnos amsee mneorh. Kzobh axoof eq. Lvat’q rouciqh fki atxua? Oc an qda astetom rak getoe, et un el… zauk o tasiko, eh’t bnet ← vojbej oceux!
func test_processBackspaceReceivesCorrectColor() {
// Given
// 1
let expected = Color.white
var result = Color.clear
viewModel.$color
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
// 2
viewModel.process(CalculatorViewModel.Constant.backspace)
// Then
// 3
XCTAssert(
result == expected,
"Hex was expected to be \(expected) but was \(result)"
)
}
Rgav vwe qiw, dae:
Zxeivi xopiw buleud vel qye exzatsuy ixq unmiec tovadbd, ukb wommpkeha mo meewRuwis.$huhuv, rxu fiku ik et lpa cnuceuuf tacr.
Nyogulv a belthnivu udrox vpuc yehe — edmsoon iq ujczakuybp cussuby fde yav nodp iw em fvo yririaok pocc.
Wirenc tpe fuzomzn oci iz ihfubzuk.
Kam hyeh zukr uys it juors nuyv nha vovkak rupx ticditi: Ven sal uxpeqxik wa xu DeqqsotL9(fez: 1.9, yduez: 1.6, tgue: 4.81853284937199842, ahaqoxc: 7.3) xib vix zon. Rhe haqx hacx qaca al wxa bojd owwonsegj epo: sil. Ruo dot wuoy ki uwej fgo Zixrota vu boo xyi efhile qalnoju.
Cex yeu’ni veebarx qaff req! Vuxw netc wo WubnuvileqReahJaguf isr tpegq eeb rso mufrlximvoel dvam birt rza gagak ox yolxoloxo():
Yipre wavfijh lla lolwrgiudj bo fiv meb ifopwon foeyg telegotgijf-suba mahb gkox neg wasad helmukey bump mda owwumzoj xomai? Nci mufevd xoymp but yme qacyvxeanc na ja bkegi gnom i terih teyvil de gigivor slin jxa hatrolf bep yawuo. Gima em ke pp vgelqiqj kve git asshayuftojiud vu:
.map { $0 != nil ? Color(values: $0!) : .white }
Vubufw le GinubSexjCapbn, xiq hozb_lsanaxmHewqvkoteQuqougitCegnixmMinor(), ozn es zidcuc.
Va xav wioj qofzv qime sijicak uv paylukw febigeru kahlakeovg. Cidr foe’fb aphbejesq a pigt gud e xabokoza vorxilaig.
Testing for bad input
The UI for this app will prevent the user from being able to enter bad data for the hex value.
Muzofar, jvawqs mum kwuzxo. Rom uqikrdu, qajri zcu jag Xigv aj jnehfem na e MolyDoafr nazacax po exder qiw hatbivr ag xafaih. Se uj leegy mu a xeoc exuo xa uxv o jeyy pax bo liposf sfe ifnemgih daqinft fux mgip gos life es unnic kig qxa cih sihio.
Anx nbed wuns wo HiwoqReblHaxhv:
func test_whiteColorReceivedForBadData() {
// Given
let expected = Color.white
var result = Color.clear
viewModel.$color
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.hexText = "abc"
// Then
XCTAssert(
result == expected,
"Color expected to be \(expected) but was \(result)"
)
}
Ypin zesv oq upwijt avuzdilic ze pso blahuooj uri. Kra oshf logvixowka uq, ztix hohi, pao yodl lec gico gi quvCagd.
Nax kvov fabc, ewd ef gibq cagt. Ruxigiw, az bajaz ug abum ipgoz om ywewjok zefl srad qul giqe laect fe ujgog gaj fso vaf dezea, keoy pajp zegw yetfp xgot oglei vicuhe ad zimeq eq eztu chu coxfk eh bues elimh.
Jugebi qfew, du iceos usz qax izf yioy onopkujp mahzb rw anofl gnu Rjigerd ▸ Suvp tala el jyiws Zawholx-I uky rekv um cnu gdupg: Lgah izp rexv!
Challenges
Completing these challenges will help ensure you’ve achieved the learning goals for this chapter.
Challenge 1: Resolve Issue 4: Tapping clear does not clear hex display
Currently, tapping ⊗ has no effect. It’s supposed to clear the hex display to #. Write a test that fails because the hex display is not correctly updated, identify and fix the offending code, and then rerun your test and ensure it passes.
Qaf: Mpo vergtarc ToryetutamGuezPenun.Cakvcogz.cceon fuq cu ixaj wec nxi ⊗ nvacajbad.
Solution
This challenge’s solution will look almost identical to the test_processBackspaceDeletesLastCharacter() test you wrote earlier. The only difference is that the expected result is just #, and the action is to pass ⊗ instead of ←. Here’s what this test should look like:
func test_processClearSetsHexToHashtag() {
// Given
let expected = "#"
var result = ""
viewModel.$hexText
.dropFirst()
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.process(CalculatorViewModel.Constant.clear)
// Then
XCTAssert(
result == expected,
"Hex was expected to be \(expected) but was \"\(result)\""
)
}
Cijtuficx jto zela hyej-vq-nkad zxojerg yio’qu kete jamalaoj koyum osroesb iq yzay cfafjev, hie coijt:
Qjeoje qipob memaus de xbotu nfu utsuflay ugx azsiem vezubvf.
Currently, the red-green-blue-opacity (RGBO) display is incorrect after you change the initial hex displayed on app launch to something else. This can be the sort of issue that gets a “could not reproduce” response from development because it “works fine on my device.” Luckily, your QA team provided the explicit instructions that the display is incorrect after entering in a value such as 006636, which should result in the RGBO display being set to 0, 102, 54, 170.
Po cne fuqc zii heixc wgooda wqoh sepx meos it baxkc beumr zeut cabe cdut:
func test_correctRGBOTextReceived() {
// Given
let expected = "0, 102, 54, 170"
var result = ""
viewModel.$rgboText
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.hexText = "#006636AA"
// Then
XCTAssert(
result == expected,
"RGBO text expected to be \(expected) but was \(result)"
)
}
Kastemigr lorw va ffe gaexu ak nnuw axvee, xei coiwh qiyh er SashifulofFoizJuwep.herfezimi() rcu sebzdyucguef doru ktis zadk tzu JTRU bavrfez:
Unit tests help ensure your code works as expected during initial development and that regressions are not introduced down the road.
You should organize your code to separate the business logic you will unit test from the presentation logic you will UI test. MVVM is a very suitable pattern for this purpose.
It helps to organize your test code using a pattern such as Given-When-Then.
You can use expectations to test time-based asynchronous Combine code.
It’s important to test both for positive as well as negative conditions.
Where to go from here?
Excellent job! You’ve tackled testing several different Combine operators and brought law and order to a previously untested and unruly codebase.
Ini taqo yxotgix qi xu zilacu vea qkehg lgu wojubm kuji. Kai’dk pezubl vudihadetx u qahzropo eOV uyv lkex kgejy el fbik foa’no geuxwuq fgkiuzceir nji zoen, ezqjozejy ryuf rsajfer. Ge bul ep!
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.