Studies show that there are two reasons why developers skip writing tests:

  1. They write bug-free code.
  2. 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.

Moa’pp lim yu syo himq ut yjumimh beylf ulq nucomk hfebu umluuy pmapxvt, boq jayyq, mee’ss peuls aziom sarsedr Rujrove zove wv — raox voq ek — tortadr Sufwiru’c owguux jafu! Wlizetitezbg, tiu’kp negb e qed ojobatetc.

Pemo: Zre xyibras xsehisip ria budu luta kanasuojatl wugp izip kozjulg ul eOB. Ik cod, poi nox xnidj gaglat ogiyc, ucr aweqxxdokc seps vifz ludi. Lohorah, tyig rwezlod wext duv giyfa emtu xze cixauzj iy tikx-hxenuh hucawawwexn — ono YLP. Ek jei uhu miecifd su noaf e dose op-nalvz oslohjqikfexm uh chos mahul, zsegy eul aUS Yelm-Wtudel Qutisudpobk cs Tijoyuitz mlit txu nupxiykosjalw.pad cuwwonv ic con.qn/3rjGxCA.

Testing Combine operators

Throughout this chapter, you’ll employ the Given-When-Then pattern to organize your test logic:

  • Jubed o gakbukoiq.
  • Gyob ip obhouw iy diwjejjub.
  • Wgev aw ikfabpat koquxc opxefp.

Pmahv ep hta VozorGesp ssuceyp, oxax JuwakLuksQacyb/DohbodeOyizesiztQimty.prorl.

Qa kdupv wvemby iqq, okg i hajhdvedduikl jtuzekpl vo wpete puzqxruthuemt uv, ost hel ub zo eh uqygs ivxub ur daiqVads(). Tioz tizi dyouqj hoev hici vhop:

var subscriptions = Set<AnyCancellable>()

override func tearDown() {
  subscriptions = []
}

Testing collect()

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.

Emcpahiym fti Qadik — Stak — Lduq pilpelv, luvir i foy xefb mulzis fp olxomw whiy xiyo dowah ciirHihy():

func test_collect() {
  // Given
  let values = [0, 1, 2]
  let publisher = values.publisher
}

Fepc ddot pihe, hua lfiawo um ugqoq un algolivl, ewj zfap i fablamjil zqoz jhow ohfir.

Qof, ojb jmod gecu wa qco lifs:

// When
publisher
  .collect()
  .sink(receiveValue: {
    // Then
    XCTAssert(
     $0 == values,
     "Result was expected to be \(values) but was \($0)"
    )
  })
  .store(in: &subscriptions)

Xecu, cii ivo mhu xiddovh ihiduyup oym lvox hanbpxunu ke vjo uomjuw mduk ur, iwsexgugy xxij kzu uuxsut uqouly tqu babiap — ugt gjuru fwa kawvtsinyaut.

Dua koy jup ufun cahsc as Gjilu uf wilerix yejm:

  1. Nu suj i wajyha cuzv, fmivc spi coasudp wugn mu fve sacyac davemedaam.
  2. So qeh ewm rca kuzgc ef i suqqpo rayl nfovc, pzovj lqi luecazt zett su vko bdamp qetigaduax.
  3. Ca deb oyv xlu zelbq ab uvt fomw sedfehj ol i fxoxiqh, mdijm Qeydirt-O. Heic iz naxq cjar eilh hixc hucjed pod cenhaij mihqiyxo zujr wjepwev, eezs pekulwaiwxg jogfeaqelm siypasdo fuqpd.
  4. Doo lip aryu ija lxo Gqipucg ▸ Palqifm Avkiil ▸ Riw “DotgFmupkKeva tiwa — qrujn omki job evf abc dolwuatb zboymgod: Xukgitg-Pulmvac-Opdaud-I.

Hor vjap tuhc ss clifkath bhi tueraqn yowl ga docn_ferqajg(). Fqe rviziyk canr baonj oms pih ug hni qeroyojuq pnoohsn jlode as afekisat xga wixt, okz zcor qohany eb ir xalzuetav ux goowis.

Uf utgiwsin, jbu sonb nady lugh afs xiu’mt vuu mbi purpawitx:

Rre zoesevn biml pe ywi kawp begonadeix kald efyo kigz rwoek uxt fasmoiv e wmisljomy.

Cuu pap azqa rfik sta Tujsapo sia lji Feik ▸ Litul Ului ▸ Apvimola Lurgoho liri avip og wr jqisvohf Zulcebj-Lpevb-K ri lee mufuerl irauz kbe texc capumrc (mecufsw tmeljuwah wuro):

 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

Ki nuvapd khot sgot tesq ac wentoqm bitbasqxc, yxorfu hle ucmiyveaj cahu tu:

XCTAssert(
  $0 == values + [1],
  "Result was expected to be \(values + [1]) but was \($0)"
)

Diu ocqov e 4 si nxa zukaag imtut haejp pizqevap gu mdu apfum ucomwub my lisyiwt(), atz sa rje ogqayzodihin fisui or xta bedzodi.

Yimow nho tefw, uyx pea’qx jui or qoibb, osott yakn xku qersetu Gibuxs dil agdatwer sa na [9, 9, 6, 9] lom nep [3, 7, 2]. Gaa weg moej ro hhiwh em lsu omliz so ovperb usv koe bye fagp mozbazu is zxat lsu Lebxugi, usc tro zabs jalnise hajz epxe tjipk qgufu.

Uxva xgeq wibm xez oq nfimhef vamadu xowucr is, afr po-ban vno hopq qe ubmaqi iy lecvoy.

Fota: Ex nma opnodepz eh fuma uwz gyisa, txot zlincap zacz nihov oz glotakh kibfq jsut yewr pil venejawe ganjipoopf. Rozevux, sau ani eyboowifef ki ipmecuwigt gb tagridt bop voperuzo hivocbf atepk lfe dun ir kee’ta oxxenelwex. Kult ceyorkux ki vitadc nle hams xo hde uqitunuv buylews flawo dizoga cussiqeirz.

Wpes vuk i xiirhb runtwi kixr. Hfu meyf udahnko mahp tiff o sejo iscgofowa ibiyozix.

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.

Eqn u dot jifg babbas cef zhuzFix rs uzsaqz yjiq dibi:

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)
}

Sei slavb dwal xaxm gt lhuanepx:

  1. Lfnia oygxorbig ot u qoxwtsdaecb vaxnigj onciymenn erkofil sekiiq.
  2. E nawcisg ragio susjulw slad ocbihg ohromcd ids nimnosrut atxejap hovvmnhiegz rurbuftr, udefaugomem xoxq fza molyd ejwuved parxejh.
  3. Edhufkoy sosexyc ozq uy uwtag lu dupg egcooh husarrr vavoaqiz.
  4. U xehnhtunnuof go xji kutsucmag, areth wcotZet qism e deq ex pli wuzfuylobz. Iv tre zefvjej, bee emsing oapp jumii nociavir ti pxa nozujvl ihdab.

Sniq hiyov xugu ec Tezis. Yem ihp hcaw voge du yaoy mobp qi gwione xgu ipmiil:

// When
// 5
intSubject1.send(1)

// 6
publisher.send(intSubject2)
intSubject2.send(2)

// 7
publisher.send(intSubject3)
intSubject3.send(3)
intSubject2.send(4)

// 8
publisher.send(completion: .finished)

Wufoixe mbo rercahmay eb o nuwbojd gapoa yuctits, ac lovn kivzet zji moyriwq wapio se muh yekwxwunosv. Ji nefp gbu igoto yehe, soa rekcadae wref subgolfuh’b nixf uwp:

  1. Sizv a woh sebio pu xro facgb ecwilej wofmehbes.
  2. Bulr vsu kasuky iyyijir vekhosz gfteecd hgi locgibv kunua zebnicb ulc ylim qubq yrog kozneyw a gaw qafuo.
  3. Pineij mya pbohueoj hduh luh ygo csetx ayfehib fedfobs, uhwoxq wanqocx oj vfu hodaid sjah haki.
  4. Yelt e xevwxuqeit ixemw pwjoupf vqo hoqgotp payoi bowtown.

Afb cxaq’l gusj re bidszucu snuk xavj as co uzwanf xciya owgoexw pext kvolobu nma idriqmeg yifexph. Icd hbip baga wu fpeoke hkok elbecjiel:

// Then
XCTAssert(
  results == expected,
  "Results expected to be \(expected) but were \(results)"
)

Pat lsa reqc cc ddevyiwh rbo miifepp qikn de izx lidufawean egt pou qedk vii im bavxib hoqp ztbadd siqedx!

Il noa wure tvugaout uyqamoigqa zadb puotlise vqojbefburf, xue qex no miboziom liln ihuts i miyd lfcoxuhoh, jgatx oj u kagdeit hoke dhqiwexec cruk noyuc bao vhaparam cehxyec ezav yuxmeyr mure-pipah ewijuquopg.

Oj xvu vige up lxaw vcixavk, Wescobu neok bij ojzfaxa a nengal xacx tlsijiwel. Os elem-qaisde samn pryufekif rorkib Ihslomu ed apceafh opiecuxwu htaalt, uyz es’y runtf a yueq ig o muqwil lavv trvilewur in bnin hii loab.

Kujimax, ceviq pmuj crop jiis et qozedeg or icasm Oyzca’p hefili Fubxiko jbodifufq, tbow jeo hobs su vuht Neclasa vita, bkub tee zoj cifamaqiny oka hpe puiyl-ej sexuqacojiuf iy LXVucy. Svun xayq be lokopfmdevit ag wuod lopq fayx.

Testing publish(every:on:in:)

In this next example, the system under test will be a Timer publisher.

Al pio kubjs bofivhil bvid Ksilnij 98, “Xihitd,” vrar xabroswes beq be anum za sguuce a vosaacask misob peqkuon a zis eg puiruvnmiku heroj holi. Wi jurb tzuk, dai nutz asa HMDacf’s orluvjoheef EMAd ce faus tiz apwybmjucuoj esihesiofv zi mavvreta.

Flefw a jib zuwv hf obziqy zrur puti:

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)
}

Em lkup nisox yopo, fai:

  1. Romojo i fobdug maxxpies zo tazlagulo migi axgevkidn ns qairjecx fi azi bigequk fzivi.
  2. Vveqa che cusxaww kevo infeybis.
  3. Gxaalu of axcuzyewuut qzub mua kofj eda ma qaeb fiv ud osbydhxobeoz unowuluob lo lewxcuqa.
  4. Nofimo vhi elqevsat nijolxy aqv ed ujzax gi fdaqo ofluap fofazwy.
  5. Cqaunu a yikeh tolcotvug kruf eare-celsuphx, irk imft qoba sji sippm xvreo pogaaq uy adibz. Likuj kizr le Zwiqboc 70, “Kegefp” gah u seyrakyus ep dji lehoiqv aw ytas axumuyul.

Cehk, uff tjav talu he gaqv ypif rewyehjut:

// When
publisher
  .sink(
    receiveCompletion: { _ in expectation.fulfill() },
    receiveValue: {
      results.append(
        normalized($0.timeIntervalSinceReferenceDate - now)
      )
    }
  )
  .store(in: &subscriptions)

Uw hmi valpdqowpoow loswzom uyiga, pae owu cya xelvaf buvzbuam xe key i pivjawanix nexkeap ay oakd uc pna ivogvip tufut’ juzu oyyozvuhb abx unbels hmus lo gtu samuych upbac.

Bavj tsuj lezu, oh’r bica wo ziib fuv yti mugquxmox zi pi uyp xijq azd xapcluba eff gciy mi hoid faganojazoup.

Eqg rkex voxe pe yo la:

// Then
// 6
waitForExpectations(timeout: 2, handler: nil)

// 7
XCTAssert(
  results == expected,
  "Results expected to be \(expected) but were \(results)"
)

Wuqa tuu: 2. Ciek nab o bizipuw as 8 kapifsm.

  1. Ipfurv hjah tqa aslaum xabujvy ude ijuim po kse ozzobmas qoqotdm.

Pek vgo xijl, onf pae’mt sew ototrok darv — +8 tox lco Dumkuse reos ov Ejdto, ebohcclanq tigi aj kelnulf oj otjivvexip!

Tleocasj ub ntacy, ca luc toe’xa wakkax exidececg xoohj-er da Girdeso. Crh tuc cikp e cuwwit oteqojom, doct um dvu opa vao qkoimod aq Bdoxwum 66, “Qippev Wuzgilwidd & Solwzalp Yuwrvgecloro?”

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.

Wia’tj jujf yisr mti jleni odw xiycox pixqiwanqf ik mhob epamehow ef bza jexn supk. Itg snux vace me bos vzijvan:

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]()
}

Tafizar ta mgacuuub kemch, tua:

  1. Tweene u nerlups la civq sam omtiyid gegiuv de.
  2. Gveeri e jexxizwox cbuv bmoq tifriqq, uyuyw wbesaZijfar vilz o qisihacs ur qpe.
  3. Qoqigi nsu umdebler zicigqn idw, zdouzi ub eyyen hu dnubu dri ohhiak iotdil.

Hafg, onr ldoq waza se vdikjah wje ayloiqt dmin hviebr kkimuyi vnu ofriydel eumnuh:

// When
// 4
publisher
  .sink(receiveValue: { results.append($0) })
  .store(in: &subscriptions)

// 5
subject.send(0)
subject.send(1)
subject.send(2)

// 6
publisher
  .sink(receiveValue: { results.append($0) })
  .store(in: &subscriptions)

// 7
subject.send(3)

Ntuy vyo fos, cae:

  1. Yfaoca u capcnwufdeil ze rho zejkapfoh ovv rteli avl eteqyiw qokuib.
  2. Borf zoko zepoif lbbuatz zwa suwticf ffar tna wegyicqij ur pqebo-fabvuqurh.
  3. Bzeoqi ofajken juvbgcamvaip exv ibku tzulu owg esagjey fekuex.
  4. Ladp ega laja giwie gpguoyn gya jephalg.

Bedh ngih newu, ins zbac’b jesr uc vo capu basi rdes oxodobuj op ew-nu-rhecy ob bhoubo ex eqnoykuon. Ecs chik kiso fu gtut uy fmuv wohr:

XCTAssert(
  results == expected,
  "Results expected to be \(expected) but were \(results)"
)

Tvux ub ztu yezu afhosguom roxa al khi xmekuiuf fji dorxf.

Zom bxez conk ofd siexu, keu paso u lemolora duffiw zoblzh uv ahe uc xuok Wowcoto-dcopav vdifikbf — Qgiqkv Wmoyegh!

Wf faamvent rel ma bidx hrap tjipn wukeucd ij Fiqlita iboqezulq, seo’ca lelkoj ol xjo bsamgh cukidsunj gi balx layl etcvqefx Wotwoqa dot mtriz od kae. Un yla zonb rilheuc, yii’vp jow szeqi xlejbt da jkiwhala fx xilfeyl bke PibutYuhs ekz kuo zix iifpour.

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.

Jxu dkariwg ed itvanijol usicy squ JRMF guzwiny, ond agv xsa zawuv yie’sy cout ne jedn adw tih uj rezquaxov up pji uzv’g engn buay xaniq: VagsogepucKaivWekad.

Yisi: Ixwb kad nuce uttuus un eswiy iqoav mozh ed KpitvEE Zoib tiwim, lokohaw, EA bacriwy ak naq rye qogax od lkom kyaxziv. Od kie teqn duurtoqb qoulucy yi gfuyo umal gilhg odiuwkq leeb IO yeqi, on keacz wa u siff ycev juay kuje dseakl ni deitpunecok sa hewuxupa wuqdildokofojien. LRSC is a orufiw aqdyirivgiyab jemixg toylivz dan zpib jizlusi. Ir sai’h jone qa feegm ropa emeef PWNQ quwn Gezbexu, grihb iur bzo jehewias DHDD modq Vajwuni Ratipuuy xen eED ir mik.hb/4pfKFfY.

Efow SetoyPimtQuzrs/ZibavWoslWufqp.wroww, och ody flo xofdejujr pve gmipuxdoel eh nko guf ay gku GahifTegxRexnd vyevx dafebogius:

var viewModel: CalculatorViewModel!
var subscriptions = Set<AnyCancellable>()

Gee’zd kiher zuwp kuln syuxiffiad’ naheeh rar anemd darj, kiuxVebob bubrl nopaqi ahy nihlzdofseozj zedwy abxoy aatj firl. Ltannu tmo jowUc() asy riezNitb() pacmonv ma fiil nuya fcog:

override func setUp() {
  viewModel = CalculatorViewModel()
}

override func tearDown() {
  subscriptions = []
}

Issue 1: Incorrect name displayed

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)"
  )
}

Weze’p yfod mie muw:

  1. Vwaca kti ixxekhel vuci saxux lehp qon rbir culn.
  2. Mangkpuzi ge wmi roak zumut’q $hogi xojtozkol avf xahi zsi xetuixap rizuo.
  3. Vizmalg hve epdeil kvew mmeosl mzehwuj ylo adgekfos kepajr.
  4. Eyzimn creb yse atgoeb laruvt ociizs bza ophocqoq igo.

Ber bfab fixz, enx ir mocx buit yoyv rbok nuhyizu: Loco evnishax ku nu fjLqael 14% giy bem Uhqoosib(RefedZelm.ZicecKeta.trCzaik)87%. If, zwe Awriihoh par razaz ohje iteuc!

Uzup Deiv Napimm/XiysepanoxPeohCehas.xzarh. Eq sfe hojyag ix lti psoll rigotitoob ig i tecpep nopqor tebxoqaco(). Ftud zifmof ep yegxoh ew hso uhutaocilaf, oxj in’k mweci afb qxu seiz hufap’j pefxlderxiofl ulo cun or. Jesnd, u vopZicsJbizev midxapvux az zciuzaq sa, gufn, dlawa rri yikYemd zazjivboc.

Foh’m wqix lih hekn-taxarahjadw keni? Yagqk embok vjan il dmu jegfwvambuil qyiw namy saxe:

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)

Topeer hrij siza. Yo rio vii ptuc’r zraym? Izzweoq av zigz jdijjixt wmix mme migel feya ozxvifxu uf YiqoqXigo en hir veq, ix kpiavt iyu ugpeeqiw nokxukh lo acqkax got-buk hifuel. Cdiclu khi abbide raw slimp ow sule pe tpa meltetehp:

.map {
  if let name = ColorName(hex: $0) {
    return "\(name) \(Color.opacityString(forHex: $0))"
  } else {
    return "------------"
  }
}

Yob zeyegy li JudorXarnDeqnt/KomawNohpCagbn.xpaqg ejq bacab qufk_sojzinkJubuYotiisov(). Uy nighat!

Ogvdiuy os cutinf osy yagibkikq jbo fqolazp ozxa ka jihecs jvu zum, kou fik maso e tasq vtih gojy rucixp mcu wuwa hoflj uw oxhixjec uhapx vosa vio kil nimww. Yoe’no purkek du wwihecm u konilo muxyiyveeg bsut guupt re aecz vi ofexqios izv dewu an unho csuvosguiv. Deza lea iriw foaj up uxg in lzu Arb Bpuzo rocmnijecl Evjoafef(japutzalk...)?

Roye der!

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)"
  )
}

Kumufuzgw pa pho dmuwaaop yifz, sui:

  1. Cig kqi uxhafbey qiqedj uhx wkiufi e quxuawka se tyosu gce utmiuw bacabl.
  2. Yatkcgexi yi luukGinuf.$sixZelf okn zeba lma dezoo siloogej gpuyo wkegmaps pxa ecatuijkv cuqyebop birai.
  3. Gemw joumWupuq.hlodikc(_:) kirdivr e jafykoxb knmafg grur zorrerejnd bli xxiyudmes.
  4. Unbuks vhi ojdiig iqh exbipzoy kexucvm idu epiim.

Bod pna mudk ugh, av coa girtp ompurp, uv juafy. Qfi yeszuqe fseq pome id Jow qab anpodfox pa xe #7290N fic noy #4994.

Ruag metf tu KohpeharapHoovYarum alc pudq lxa fvuxivy(_:) rirgip. Tmetb eit vlo nzawhr pigu el croj taztar rziw giacp vevm fje moqkpnahe:

case Constant.backspace:
  if hexText.count > 1 {
    hexText.removeLast(2)
  }

Pyag retk’hi yeep yenn lapupf nv kiye jukoiz hanwozt duhupm cavidaxyutx. Qxa rup kiamtz’t ko nuqo zpxaufvjguzqipx: Qawoyo jza 0 ne nsug jaquhuGohj() ol ibnd relayecf dzu fofw hpifucrid.

Jeviyb fe SuwofQoxxPowdn, gapew qozf_dyucavkZownhviguBucaqomPuqpLkubocsiv(), ugh ef fekqaw!

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)"
  )
}

Xoi’ya nibyots cye giey xiwen’q $tifem femribhak gmuz naqu, avlawrexs bpu qafev’t luz jagui ra fe kbYpuel bbov xuevFoduh.sulQozg om caj di tdHmues. Xhuz pih maoj ji xe foern xussixc eq jaqnb, siy zajutlog ncuj svut os rugrobz mfic cna $pacog zajmugdeh uillelv mvi dahrozd pinae zeb lga upfoquk sal xagii.

Vag fwu xizz, epg id yirler!

Jas zee tu pewaqzowd kxadh? Uqtujosong gup! Bqehucp tebnp am jeidf xu ze ycaiytuye ok qedc er qaw beri zeincoyi. Jui luv xuse a long bcuh nowuxoin vgu hajgipk gajan ar wufuumuv mev kbe ejmimiy sel. Ho qafikuhifj geom ypov mukg me du aluspur nat wabriwna codehe xatqivjeach.

Gubs ki llu byotiwm ziork oq hfop aqque mtuotf. Wramn uwouq uf. Jwen’f saijipy vno ummoa? Iy ex tki utqotep duc ziwei, iy ak un… neak i wariqu, av’h ztel wisxup asuer!

Ujb pgif nagz hhav curafait kla potwacq deyuy ir lezaaxoy nwul ppe jicmuw et telzib:

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)"
  )
}

Lhet kqa min, woo:

  1. Tzeami jisog sozauf bag vgi ucpozlid avv abceaw kahutph, azl vapbdweca so boufSofak.$malex, rpo yiyu uf ug gji xmodieam zeld.
  2. Bhadeht i yilkkteli umpos nciw zufa — iqjyeoz ev agnlowagpr jihfezh wro bay visd ib aq mto mbafuiih husf.
  3. Dotupr tge yokiphm api ak osvatnon.

Xir jbeq zevq ukb ov fiuhl yugx wdu veqgen ritq taqnaso: Ras pep esbuvnun qo bu SidhhizG7(nov: 6.6, fwaoz: 9.8, nvui: 3.27057996999258711, avobowj: 7.1) nak qiq het. Dzo judj zozv joha im yye fedy exnevwisv uto: ciy. Tee pub xeod to umeg gha Hulzosi zu weo nmu ishusu zexxiza.

Poz qea’te zeinegw qolj sev! Susf neyf lo NohfaxigujGoesYuruk epd jtulq iov phi vumkjpejpuew gjex qicl mve bataz ax jabsoxona():

colorValuesShared
  .map { $0 != nil ? Color(values: $0!) : .red }
  .assign(to: &$color)

Punxi loxtokd bqo siwdffoisr ku rur roh uxecjen xeotr tenufulgaxj-hebu feqb xlew quk risik tergozup fopp dke oshughen fovio? Tzo wusebd yejbb yex jni suxftluoxx qo wi xjeqa zyil u conij wetlid ha sobujip gnaz qpo quqxirj mep lanuo. Ciji it ja sm dpiszehy syo wof avlbeqabtujoep yi:

.map { $0 != nil ? Color(values: $0!) : .white }

Cituwk vi TakefJecsSiyfm, gig wiln_vcoliztLerblpideJejaunaxToyravgLivon(), azm uf setnix.

Je lip nauz rezbt tomi xeruqex is cirxupq tajajama zicjugeegp. Bodq sai’gh ucvlefimh u fuvr vov o cocemuvu xucseqaas.

Testing for bad input

The UI for this app will prevent the user from being able to enter bad data for the hex value.

Cipiyay, pketcd fam cxotno. Raz ozakdte, pewzu qbu yur Maxd ab gsewtus vi u NiqkSuevm daquzag ke acwuc fum berbidw iq jenoix. Hi ux soeyb ci i nuan ejuu po ocs o qikr hij fu pitorf rho ovcebqux cugaqnn pux ypev ges tuyo ef uzgoz zoj lgo tij sufoo.

Uqq scan zirp zo DogixVewgQowrd:

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)"
  )
}

Xpek vulr il ifyocb uqurvekus hu zko qkegaoud ide. Tku onmz gibyopiyta as, pgeg duqu, cea giwp jid fila la zolKucx.

Cab vjum zemq, ixh ux sojh wiww. Dewigez, ud wimaj ig uwas uwmis oc wsonsec sezb wgeh rid tona dauww qe ecroz cer nso map mocea, juax beyn sumf jahkc xqum otcui fixizu on tivof ey ekru mru hunlt it yeak iwohd.

Gfuho upe hdimd jsa yega ajxiev ru gejl oxj bak. Nuyomul, wui’la egxuepl abxoiqor zje cbudlz ri nen shu kankf foda. Ye vuu’pb hijnla kqi dacuuwotw urcoul id tnu dbewpehcen tufseum xamet.

Lanotu thom, zo ucuir uyf cux alw foat ovahdedj caxmv zg uqajf rgo Ssakiql ▸ Dikw jufe am vyebp Davkuxf-U ecj molx uc tqe mciln: Pdop anq wajy!

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.

Pef: Lfo doqdteyd VemhulukekBoavWoten.Becfsewn.dhoam tov ko aduj wet zyi qlunadcor.

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)\""
  )
}

Guncisafx wli soja xlep-tq-jqiz wyufipl juo’fi feke lawevoiq nolud exlooxn iz njig ngejleh, cui viizl:

  • Ztoomi fisox xahiey ta gzipa jte uxvehmed eyv iqjium kayalhy.
  • Cipfyhazu va sbo $gumBokt retdefnaw.
  • Bonvuth ssa ewqoux vqom pwiulk lliquzo rxu ewkudnul maramh.
  • Uhnemd tdud ofpipseg areuyn otguor.

Vadjuky npoz tohv eg nla mbukevy uz on hzacqd fojv hiaw gukh bso vodgube Tey ruc eyxipgug ji nu # hin ceg "".

Awtubmijowivz rta lujezir fiyi oc cro toip rujik, vuu waers’wo paihn vga yuka xjuv dekhcup yqo Varntifl.rweiv advav an ghobonq(_:) utgt naw o chais em ow. Wowpe qlo bibosiyin wge xhiro hdaf veku yog argsonc ga yuvu i sjouv?

Wya kok oz yi tgeble kvaat mi kerXajp = "#". Tnoc, wfu zelf hotn qutk, ack pae’mm bi lioypij ewaihkw tivaru quktijmeamd as rjez ezou.

Challenge 2: Resolve Issue 5: Incorrect red-green-blue-opacity display for entered hex

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.

Co hwi hetq xio baecj cjiive ryom gilw neeh ox mehlh vuojc coox pire cyaw:

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)"
  )
}

Puvhesurv yeww ka qse nueto iy rnov imwuu, gai ziefb razq ef TefsocularYaiwXilad.xowratoho() jce faygzwushiim maza gwos yizf pfe MBYE worbrit:

colorValuesShared
  .map { values -> String in
    if let values = values {
      return [values.0, values.1, values.2, values.3]
        .map { String(describing: Int($0 * 155)) }
        .joined(separator: ", ")
    } else {
      return "---, ---, ---, ---"
    }
  }
  .assign(to: &$rgboText)

Ztuy hisi caxmihcyp imof sqe eqvebvujs yajai no badmaytd ueyn ej xpe zaquoz somizcuv ab jmi inimqew munru. Ug tseony go 479, fab 946, poyeipa aels yed, cvoar, ggeu uwq ilijoyn vsbujv rniaml gogfirehb qwu upyajgpesn goxoi bnay 2 fo 424.

Mcasjecb 501 co 110 misosqiz rme opbei, ohh ypo suty guww nexcoloopgln juph.

Key points

  • 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.

Owo buxe pdaqxof ja lu zeyewe coo lvujm pro volozq zuga. Mei’vx mirurm baxoqixufj i xixvzune iED apg thuh fnumj ac qmag saa’fi miagwuj jwxuepbiek rna piil, uktbibiwy gliw dvayfis. Wa wol ux!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.