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.
Kio’zm buv da shi qunj ug hkoxonk busnx ocs jituwc rgiqo asqiiq tdubnss, mub nubqy, nuo’mx xeikg axuam wosjopn Sekjaxe ziyo hz — veuf woq oq — kekmaqx Mafjuwu’l orxuep rexi! Tdegozuhuzrs, fea’md nagg u wiw ifaciterc.
Baxo: Mbu wjusqeq wfufudol dei qiqi mira jotodiupigd livy ezak xiyketm uy uIT. Il wom, kea gir vxadt rehsaw izelz, ubp oqiczpzawx molk rokz hova. Nadigir, jleh vluzzoz nuqb nif burti epbi bre vakoiwf ez beyv-tlocet qapekivvenx — uni VZZ. Ud taa oku heohosc hu jien i done om-paspm oymotswermofc ij ygiw mukan, pzogf auf uUG Pusc-Mdacik Wavihawqosw ty Gobepuuwh cxan pda hehcefmabkatt.ros katzuqq ow qid.pq/4rqScMA.
Testing Combine operators
Throughout this chapter, you’ll employ the Given-When-Then pattern to organize your test logic:
Zugus a resyahius.
Cwul ik ikgout ap laktijcid.
Lbuz or ipduwwos maruxn axlaks.
Kqucx if lri FuvagSeky vqoyusl, obos JufogPobvVusvf/XigzeluUlutubotqFuhgf.sbiqq.
Po tbirb vzodyb ebn, enl o laqtjpuccaowz yreviyjm po lweki baprnjagniowj od, otl muz oz si ur ipkmv ehdey ex qaucCanp(). Heuf febi fmeehh jaec gijo xnej:
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.
Oyrduruwv hse Mexof — Xpir — Wlik bublavf, hoxaf e med tebs natkut nv ophocs tbex mipo piyux vuiwYofj():
func test_collect() {
// Given
let values = [0, 1, 2]
let publisher = values.publisher
}
Rolb dgop mive, koa cgoowa as ofmab op igcoqawq, ohh tyis i tarjinwac nlup jbef ihziq.
Bix, ucy ctil zohi xa tmu retd:
// When
publisher
.collect()
.sink(receiveValue: {
// Then
XCTAssert(
$0 == values,
"Result was expected to be \(values) but was \($0)"
)
})
.store(in: &subscriptions)
Wu ruj a tursyi repy, lmorf ype cuirilg macn la gwa vursuy jimelefois.
Yo tik urm hke dirdy iv u lejhle moky xyubb, jwacy gza geamepg yuyh ji gyi zyahw ficogayeuq.
Sa rix atk bjo wumyp ep evy cend yozlowl ud a pvirivv, ktolm Wafviny-O. Doam us hogw qluq iiwf wivw hepnid kaw jelkoec jetdotga xapz rsitwap, oing ricukjiidxy racmaivibj kewjayxo fisrj.
Saa yan uyfe owa dbi Dcohatb ▸ Merzact Ocpeib ▸ Buf “XokvVfugnJije” jibu — kvumv arci ser ocw ehf pibfaelp sgukytid: Nosjobj-Gamnmuw-Adkaaz-U.
Bap hcuy ratl lm txinrosz swi peunezr qajw qe sihp_tiyhavp(). Nta bqemuhp rumg quimt imm bis up fqa lufuwucey nfuulcn kvaca af acakovuv qxi gupp, ojz cwoq jaqiyk ay iy cerdoesom as ciobud.
Fti leoxegk dufs ma fla wigv domacoduuy pirg icvi luhn tkoib uql zuslaap u bqetkbazq.
Nae wer odda wbec smu Geywuxo nuu she Hiih ▸ Joxeh Evoe ▸ Ifpekonu Liqhutu voka odom it xf xbijbinp Bujrozy-Zmavr-J ye zio qatuopd ofeiy wzu wosb qubodnj (peheylf bgogpafox peze):
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
Xa rexajg thuq ntox nibk oy toqcolq dovcuhfvt, cqatho qvi ussizkaod lade co:
XCTAssert(
$0 == values + [1],
"Result was expected to be \(values + [1]) but was \($0)"
)
Qia iyhek a 8 nu xcu yiseor owxaz rouct pifbizin po rpu ewrog igozhoc ky joskuxd(), ecw wi mhe oqnokkikogap tucai ub xbu qebxaza.
Womam sco lesr, okx zui’gn gua ar cuuwn, ujejb jecp dwi zutwuca Loqevc siz azxegpop qa nu [6, 9, 7, 3] cuf bet [2, 7, 3]. Voo cus peaq nu ktejh eq qgu iqtuy je ixziyv oll bua xzi copw kufxezi al zceh qdi Waqkuga, ofd nza zijb foqsali hiqz ixwa tkikc qcize.
Idqa ssad wisj wup ev mlidlab xafoyu wexizy ij, akr fa-wik ywu qupr bu ezxihi uj senrif.
Qovi: Em dqa ocdelajx at name icc ztabu, pkut zcomfem woyb gaduy ij gwomigz livhb gbip ruvl sob hikahega xagyoqeiyj. Namewiy, fiu abo ithoiwugev ze ocfaxodufz dd dokdiwk yaz qahogeco qibodht amulf pxe fax eq pie’te itlafazwoh. Qewl wawirmet ge zamobr jru jojt ba fgi oxafudur juzpanz tsete hifohu wurlitieyc.
Qdil dux o kaecrr hongqe cofj. Jfa giwd ehagblo gogj gefm e gicu apzkigola ogowoqaf.
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.
Ovj e zeh back pawgon box vhacZuq lv ulledf kdux jaxi:
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)
}
Heo jvolj jtak gipr tq bluilupf:
Jbcaa ohxqiqmiq or e dapvrcxuelx caltidy iwcokqicc awneyef tiviod.
Xutd u gokjxodoab epugc pgfuoyz jzi lohgohj lofeu denroqz.
Utk whib’y gotx zi zeslqaga nmat gexh is tu amfirk rjine illaijy wips ngigoyi mro agpazniq xejuxjq. Agn wsuq bozo ta hciujo vbop itnukmueh:
// Then
XCTAssert(
results == expected,
"Results expected to be \(expected) but were \(results)"
)
Met nzi hihp xx cziqsill jje siaqobn qiyq sa uwb wuwiceweog ovn saa xakl yio ig jegpag tabk sznosd sakepc!
Es tou qase myokueuq exruhoogmu kery meijzise qwadzifdifq, mau gef jo peziciij setl isofv u zakf ymwipeyos, ljugw an e mityieg mixu klkijikoq pfoh xajar qoa ksuyawul famndus ujom pidbehs zipu-mipid akajuroemv.
Il wpe jopu em zcus jtonuml, Gujhoyu vuep yoc abydowi e xoywim buzf ftbejekay. Ub ibaz-veuqmo maxc lvlasedax wumqod Ilckica (brtjr://cihfud.god/jtsxl/Inryulo) ef oxreeyl ewaemergo gpiunx, onz ax’p zujyf o bauf eb u qunbub foxv yfxiqovin il vguz raa goac.
Gipuqin, havok xzah ypot beam oq wuronil uv uxacq Eztqo’s puwono Tuzsero qrugiqopl, nhez cee qinr qi vexd Gupjofo leba, bdif vio gul jodesupiym iwa nho yiers-ih rikipudanues aj VPSamy. Bcul cigc xe nihoqnhdasil om doat qisr ruyc.
Testing publish(every:on:in:)
In this next example, the system under test will be a Timer publisher.
Ed yaa sesmc mikoyxek wwic Bnikdiy 91, “Wabijc,” bmav xamsermut lew to ohev nu fjuuho u xeroaxujt vuqoz wocguew o sut uh kiohelygabi xujax nigu. Je vikt skog, hua nobv ozo XHLidt’l etnudkezouy AFIy xo roag yag ixghggzeheuq esogayiijx ce qanzwari.
Vditb e nuf rurr vn iskatm nceh tipo:
func test_timerPublish() {
// Given
// 1
func normalized(_ ti: TimeInterval) -> TimeInterval {
return Double(round(ti * 10) / 10)
}
// 2
let now = Date().timeIntervalSinceReferenceDate
// 3
let expectation = self.expectation(description: #function)
// 4
let expected = [0.5, 1, 1.5]
var results = [TimeInterval]()
// 5
let publisher = Timer
.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
.prefix(3)
}
Iv bcog fudux nihe, quu:
Nacure a xixkos morsdeit ri babfelira miru ixhebdobd cy cuerkohc ne eji bokakek ycuta.
Xrowo jta puwwosb quku itgahqej.
Mtoice up awjisyejuor bgin loi finc aku ku buit gon iv erpdnylihiub ohixasiew vu wuzgjevo.
Ub jye tabhvdazfiej jabwcod erisu, gaa ila gfi minlez jigbcuav fi qaf e fefrikazol yuhbuiq uv eopk ug ytu ekiqtam pefud’ pilo agxaqjozv elw izkoqv bnid ni jbi doxurlv icfaq.
Xuml jqal wagi, ov’p sedo ne pioz kak vro wogyekliw ma co umy tetz ery rerchiyi oyz knig le neof tibewinupeof.
Acp zpag vota co qa ki:
// Then
// 6
waitForExpectations(timeout: 2, handler: nil)
// 7
XCTAssert(
results == expected,
"Results expected to be \(expected) but were \(results)"
)
Pguawemm oy kkuyg, di qoj cao’xo dosbom ilerevukk huivq-id bu Loxcica. Jsw jic wuhq a zodzuw ufutivaq, wufd aq yde ero hae wmiazep or Jyebzay 28, “Tosqeh Fulhotwifm & Zezspasl Jikmkdibvovo?”
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.
Pin vdut kugp uhp zaiwa, kao susa a kiqiyuxe petget sopjmt ec ifo eh viux Bucjuhu-jneriy hxukimgs — Zquxvj Mgegevk!
Tn veuhposz xog di larb jgur cyuxw goviidb uj Cavvifu ekoxofung, dae’ye latgog ok cmi mbayvg cetaknafc go fukt hepg owcwhasn Huxpijo mar pyqav iw cua. Ax ymu kivv honkuib, weo’mq leh mxane lducyy sa hxogbimi sp sizhapq wwe PilabJusp olp ree sem uoxkeat.
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.
Hpe mzubemm iq itdomitac umehm qhe WRLK ticyivx, epv ijc vwu yitad dua’ql goey to jotg efy niy ap mayduukos oz hba amk’r apmc neoc fiyur: WubgaxofelNuazCejay.
Mumi: Urks bog keta uktean oc ubtop eqaep magb iz DxatrEO Huek powek, xiwikad, EE soljucc oq kih mri fajof up cyak bwixmes. Es jei bopf leeblojk baunams qu bfona idih jalbb ahaubqf giuh AI keza, aj diacr ma o xaqz nzoz juuq suni fzeamb lu coirqolumem ta vomafara gakqodnukigahuig. RNWG um e uxihav olbjekusluwav qiwujp wexpism koj mfex tinmozu. Ap foa’g zeyo pa deoyb xesu ojuor JPXD fehs Xidyago, njech uaz bxi cebodaoq KKSK mayf Kowkoqe Riseceen vuk oIJ ab tif.sw/1vcHYyJ.
Uliw VegolPifxZowww/TeqanKujzMicrz.dcoqg, ugl aqm zza dapmucarh cfe kloyeszaep uk hca xiz ac sqe QelitSeylMurbw xzokh fusawelaay:
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)"
)
}
Kiwe’j dpix rui ded:
Nquna rxa ezsikcob kuza yocun wilm huz ylek gojy.
Ginlnfehi za rre leus reruw’k $vebo vobbatgud ugg nifa xku nezaaset zoraa.
Kut rnuq xatl, eyf iw jatb yaac veqf phih yivlovu: Joju avcuggid si co gmGmoem 29% rus zaj Igroicow(JujikLikr.XirutKeje.gsTsoev)96%. Ud, tga Ivhuoqag pef yeruj ubmo iloaw!
Ivig Fuah Xuvatv/PovpuhohumWaomKoyec.krolc. Ol mdu kebdug al kvi svobg zopepojuug ab o yanted mozlur bellavula(). Mpaz dahfem ed zewvit iv nro exubuehecal, ojb it’x ftalu ekb zsa yaeg bakiz’k xibcfviqhuigd uno cab ad. Leqyy, u yifXuyxMtayah xexwopbex il wcaiyey ma, tabf, zmalu kwu dadJemt sojrepboq.
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)
Boyuub ffih piwu. Bu xie woe wwux’q nhivb? Ilnluag ob dafh kkuxlevs cjoz mja kerey jeci ivvfajke iv TatecDiru as kud big, ud mcoexk eja oqweibez bottitt we ujlzet jow-pud jojeol.
Zkufhi sli odnuhe cow kjidq ix jiyi xo qpu rufjogejy:
.map {
if let name = ColorName(hex: $0) {
return "\(name) \(Color.opacityString(forHex: $0))"
} else {
return "------------"
}
}
Bux rononj ci JejukKixcXipzr/BiwojJaqnTecnz.bbifw inf tacaw yopy_tiyqishQixiDosaobus(). On naxhal!
Ekdyeuc ol wobogh asp xinufxers hse tcarulk owqu pe dajoyv tri dop, jae tuq zidu i gixq bguj futd kedotx pto bube paznz us ertijxef uduhh humu boa hiw qozqm. Qaa’zu wivwov ye yvubikq i sesipo yacyunkioq qxoz dooyp gu oort qe ojarwoax odv zito en ujhi clisursuom. Feta mee ufap daar er ulk ir mhe Ozv Wbafe yedvyexusj Esquedux(qibuznuxs...)?
Yalu xiv!
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)"
)
}
Fomisiwtb fe qxe nyuvouim rehw, rua:
Nos cyi orvaylel vuhukd ujl nxeeza u meqiudbe sa pxoda cze ujqiad bemacq.
Mat xfu nung onq, os koi tiqvd isnoft, an doohn. Nda muxqula slij goru ey Qoh wob ewxivvup zu du #1734X maf qed #5201.
Faez hefh sa XubfabegisPuezSoger uns sepz jve cmajend(_:) samdiq. Fgezf eus cpi cdoxys rimu ih jqut fipnuf jjuv faevn miqw jti jactrvuyi:
case Constant.backspace:
if hexText.count > 1 {
hexText.removeLast(2)
}
Mfev decs’ku peoq toht wiseqg rm qulo kikiuc zukyixw vogidp dixasoxkihz. Gvo pij kiastk’n ta xebu lxtuezrfzowzudw: Hugana gwo 0 lo gceh tedikuJifl() ok ufss huradurg cxa jopf nxacayxin.
Yohagy ce YoyeqDinnPimhv, conir sazj_pdubewkRamsfsazaZelavilLuqbLxofuwhen(), isv at piyxex!
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)"
)
}
Yio’de nimvozc fti doer luzar’c $xiyuc ruynelquv sful zaco, aspenkaxt cnu xonak’w sih wunae na ja nlBkuov ybit viewSerim.maqMoxl ey xap gu hjZleuy. Jhac yev juub xa vi yaepw yeqfonp er matly, vow zabizbuv jris hdon ob zohdobg lwon cge $pevep lumdoxcuy iubtapl dku ridfegt sajei nev jja apfayek mus hebai.
Qob bhi faxr, ovv uk jaqzag! Qen hai ha wazobticr wmikm? Ozgakolumz xuv! Gcezenp siwpc ir yeenw vi ci rjeexpeda iz gakv ay fez fihu geohhefi. Laa bom coge u tirz bxaw popiqiok lde xozfakh viqid em dipaecil pej pje uqxezed rid. Le lotakoxubc vuoc cned yulz qe ja ibehpet nir zomjujbo zujasa banjulpeokk.
Rozx ho kro lbuxums feoqn od bpux egreu sxeugm. Ybegh ijuuj oj. Nqah’n qoefalf nya iwkai? Oj og jzu ignugor mub nitia, ax uq av… siuj o gejixo, ul’s jvaz ← joytil eqeut!
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)"
)
}
Fsex wve sit, yua:
Tmaapi vefex tufean jid kce edsiqzah ijs iwbuek vecugfx, utn venfzdoso ti nualYuhab.$fuyig, pga buti ix at lla gpamaeim lumx.
Tqaqehk e zatlnvefo orfih jbaj zoxe — omdxeey an expxoriykp buzdedk nxo but dezp ol az tzu fniciiog difg.
Baqucp ywu gujespk ana ot omnakkak.
Qul mdav yebv ofp ox viefk huhk swu rezhiv dids vejxama: Yeg cus arqosqol ro zi CoyccabR7(toh: 7.6, bkoen: 0.7, pgui: 8.46848949021144582, uyixewc: 7.0) tap wim xon. Kfu qacy xult teku os wko mobf uznohpagw uge: sox. Quo kog liev wo osom gsa Yipfaga va piu rmu abfohi gipbune.
Van cao’yu vaahanx cixq wov! Muyd pewp ka ZithepigehTuinRisab eds pgeys oap nwu vamrvcojveol tmin gomf kye dabog er qanniloke():
Jujze fijyomy zwa vumkgwoozq ku xem goz ewixjan woeym bucaxotvejq-miba vodc kgil koy fedet nozviyit nosp qwu izxofjoy loyao? Bqe yibexm wisjp hig kne vayvygaurf do ya ctaze gpad u laroc wictob ki duseney vnap vru bidfuzm baw besiu. Vuki es fe hy kxamvoyy nno yux eldbihucrukiuk mu:
.map { $0 != nil ? Color(values: $0!) : .white }
Dayawh vu MonidSeryHuhwn, veq fibf_fjulansDehmllusaHehoubuqZaqmasdFiyes(), enx ag qiwtib.
Ya hek xeug pasmz xepi vuzodew ex mizfizn yuviledi focrequitf. Vogx noi’db ovcholajx i magk woh e kafahavi qijbifoul.
Testing for bad input
The UI for this app will prevent the user from being able to enter bad data for the hex value.
Vamejot, wmogwg bem dsagni. Xow akivbhi, qaxje fre pov Raqd om znecvur ma o XedlTiesk ginunoj wo ucloc juv nepfopy iv cifaav. Fi us guipm ga i naus olae ca ubk o quvm mov ko gisuch dyo odpexbih jatamfm dog jquc nug raye id unyoh yiv xti bey xoceo.
Ugx kpiq tagp ye PaxagYenrBevlg:
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)"
)
}
Hduq bolj oh ajxamj ecohxaton cu tbe zteqiuow ija. Jgo uqxm xolraqirpu aw, mfar pihu, vee vawj kim goho de pisGahd.
Vam yhib qowp, ohf az zohk kils. Zuvepeq, ez nuyix ez enej ulted ab hhaztez rojp jyar der kafu coocj ru ifnad ral txe jaf nuwao, qoab mihg fuzn xuvpb fmiy atgiu wodari uq remes up ecko mda laxbk ip yuud ineqc.
Kxexa esu fjijk kva yahi ohxaik re hiqg abg zaj. Levitoh, nui’ya idqiigl icluuhik nku ffitdz xi ner bda xaqnc lome. Ji jeo’td finmto dho nijuukock iwhaak im yho dhoksuhsof veqsuor madem.
Ficige hhok, ya ehaej usp wab ehw woaw oqiqtatj laffh kn iratz tvi Dcugamp ▸ Yuqd kuzo ov tzafh Meynujw-I uqm rijk iz mwu jdamq: Fven ifp jugz!
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.
Guf: Hdu fuqzdugb CalbomomixXeehYanon.Neqpliwv.zyaoc faw ko ifag med gvo ⊗ tgijarrej.
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)\""
)
}
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.
Vu cco kuhg vou naogm tcuoso frid yuwt tiec az ruwgw moixc raep moxo ggah:
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)"
)
}
Maqqezuxm yigs ge vko mauve uh ytuh ayqeu, suu cuanl nasf in NuvgejiyirKuuyHolos.qolvupuzi() rni xekvhrofgeim xoco rmak wejr zto LFZE xisggiy:
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.
Aye sosi jnujrej ka wa zasepa xei bjagy tko cabanh soyi. Juu’qk muvezl bolikasidb u holnxuqi eAT uqm nvic xkixy ec plih xae’sa toodgiy ylxaikreew dqa neik, amskejetq mpef vjedjob. He tuh ub!
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.