Kuinka testata heittokoodi Swiftissä

Kaikki mitä sinun tarvitsee tietää heittämistoimintojen testaamisesta XCTest-sovelluksella ja testikoodin pitämisestä puhtaana ja tukevana prosessissa.

Kuinka monta kertaa olet joutunut ottamaan haltuunsa projektin, jossa oli yksikkötestejä, mutta niitä oli vaikea ymmärtää, epätoivoisesti epäonnistua tai muuten testikohde ei edes rakentuisi?

On erittäin tärkeää pitää yksikkötestikoodi tukevana ja ylläpidettävänä, älä anna heidän hylätä ja jättää huomiotta ajan myötä.

Storytelillä yritämme tehdä yksikkötestistämme lyhyitä ja luettavissa olevia. Luonteensa takia testikoodilla on taipumus kasvaa pitkäksi ja toistuvaksi, joten on tärkeää pitää se puhtaana. Testien kanssa työskenteleminen ei ole liian tylsiä projektin kasvaessa.

Heitävä koodi voi olla hankalaa testata joskus, ja tuloksena oleva testikoodi voi päätyä rumaan. Tässä artikkelissa aion pohtia erilaisia ​​skenaarioita ja miten ratkaista ne mukavalla ja vankalla tavalla.

XCTest on tehokas kehys. Xcode 8.3: ssa Apple esitteli muutaman uuden XCTAssert-toiminnon parin tusinan nykyisten lisäksi. Vaikka niiden tarjoamat ominaisuudet sallivat useimpien asioiden tekemisen, joita kehittäjä haluaisi, jotkut asiat vaativat silti kattilalevykoodin natiivisti toimitettujen ominaisuuksien päälle.

Katsotaanpa muutamia tapauksia ja miten ne ratkaistaan.

Heittoimintojen tulosten vertailu

Tämä on helppoa. Kaikki nykyiset XCTAssert-toiminnot ottavat jo argumentteja. Jos tulos on vastaava, tämä rivi suorittaa työn:

XCTAssertEqual (kokeile x.calculateValue (), odotettu arvo)

Jos calcValue () heittää virheen, testi epäonnistuu viestillä “XCTAssertEqual epäonnistui: heitti virhe…”. Jos puhelu ei heittänyt ja kaksi arvoa eivät ole samanarvoisia, testi epäonnistuu sanomalla “XCTAssertEqual epäonnistui: a ei ole yhtä suuri kuin b”, jossa ”a” ja ”b” ovat vastaavan ja vasemman argumentin kuvaukset, vastaavasti string (kuvaava :).
Yksinkertaisesti sanottuna, kaikki XCTAssert * -arvon tarkistustoiminnot varmistavat, että puhelu ei aiheuta virheitä ja tuottaa odotetun tuloksen. Erittäin kätevä.

Toiminnot epätasapainoisilla palautustyypeillä

Usein XCTAssertEqual ja sen arvoa tarkistavat sisarukset eivät riitä.

Jos funktio ei palauta arvoa tai jos sitä voidaan jättää huomioimatta testitapauksessa, voimme käyttää uutta XCTAssertNoThrow-toimintoa, joka tuli saataville Xcode 8.3: ssa:

XCTAssertNoThrow (kokeile x.doSomething ())

Mielenkiintoista on, että ennen Xcode 8.3: n julkaisua meillä oli mukautettu toiminto, jolla oli täsmälleen sama allekirjoitus, ja meidän ei tarvinnut muuttaa mitään koodia paitsi poistamalla mukautettu toteutus.

Toinen yleinen tapaus on, kun palautettu tyyppi ei ole vastaava tai jos haluamme tarkistaa vain jotkut palautetun tuloksen ominaisuudet.
Vaikka tyyppi on tasavertainen, tasa-arvotestitapausvirhe on melkein hyödytön, kun kohteen kuvaus on hyvin pitkä: on vaikea sanoa, kenttään on väärä arvo:

Projekteissamme meillä on paljon toimintoja, jotka tuottavat monimutkaisia ​​tyyppejä, jotka eivät vastaa Equatable-standardia. Yleinen esimerkki on tietomalliobjektit - paljastamme ne protokollina piilottaaksemme sisäisen toteutuksen, emmekä halua niiden olevan yhtäläisiä. Jokaisella mallityypillä on heittoalusta, joka vie sanakirjan.

Jossain vaiheessa tajusimme, että yksikkötestimme näyttävät kauhealta. Heillä oli toistuva koodi, vaatimattomia vaihtoehtoja ja kun testit epäonnistuivat, poika kaikki oli punaista. Vielä pahemmaksi oli paljon copy-paste-pakkauksia. Koodi näytti tältä:

XCTAssertNoThrow (kokeile BookObject (sanakirja: sampleDictionary))
anna kirjan = yrittää? BookObject (sanakirja: sampleDictionary)
XCTAssertEqual (kirja?. Nimi, "...")
XCTAssertEqual (kirja? .Kuvaus, "...")
XCTAssertEqual (kirja? .Rinta, 5)
...
// "parempi" versio:
XCTAssertNoThrow (kokeile BookObject (sanakirja: sampleDictionary))
XCTAssertEqual (kokeile BookObject (sanakirja: sampleDictionary) .name, "...")
XCTAssertEqual (kokeile BookObject (sanakirja: sampleDictionary) .description, "...")
XCTAssertEqual (kokeile BookObject (sanakirja: esimerkki-sanakirja) .rajoittaminen, 5)
...
Se ei ole yhdeksän epäonnistumista, se on yksi ...

Ratkaisu osoittautui yksinkertaiseksi. Temppu oli poimia XCTAssertNoThrow'n ensimmäisen automaattisen sulkemisen tuottama tulos ja suorittaa sitten sille lisävalidoinnin sulkeminen, mutta vain siinä tapauksessa, että tulos olisi.

julkinen func XCTAssertNoThrow  (_ lauseke: @autoclosure () heittää -> T, _ viesti: String = "", tiedosto: StaticString = #file, rivi: UInt = #line, myös validateResult: (T) -> Void ) {
    func executeAndAssignResult (_ lauseke: @autoclosure () heittää -> T, kohtaan: inout T?) toistaa {
        = kokeilla lauseketta ()
    }
    var tulos: T?
    XCTAssertNoThrow (kokeile executeAndAssignResult (lauseke, kohteeseen: & tulos), viesti, tiedosto: tiedosto, rivi: rivi)
    jos r = tulos {
        validateResult (r)
    }
}

Nyt samat testit näyttivät paljon järkevämmiltä: luettavissa, voimakkaasti kirjoitettuna ja tuottaen sulavia viestejä.

Erityisten virheiden heittäminen

Joissain tapauksissa haluamme testata päinvastaista - että funktio heittää virheen. Tätä tarvitaan usein mallien ansioitumisen testaamiseen.
Voisimme jo tehdä sen olemassa olevalla XCTAssertThrowsError-toiminnolla, vaikka jos haluaisimme tarkistaa, että jokin tietty virhe on heitetty, joudumme toimittamaan sulkeminen heitetyn virheen arvioimiseksi.

Tarkastellessamme millaisia ​​tarkistuksia meillä yleensä oli, huomasimme, että kyseessä on vain kaksi: joko vertaamalla palautettua virhettä odotettuun tai vain tarkistamalla sen tyyppi. Joten loimme kaksi mukavuusfunktiota muuttaaksemme testit yhdeksi linjaksi:

public func XCTAssertThrowsError  (_ lauseke: @autoclosure () heittää -> T, odotettavissa oleva virhe: E, _ viesti: String = "", tiedosto: StaticString = # tiedosto, rivi: UInt = # line ) {
    XCTAssertThrowsError (kokeile lauseketta (), viesti, tiedosto: tiedosto, rivi: rivi, {(virhe)
        XCTAssertNotNil (virhe nimellä? E, "\ (virhe) ei ole \ (E. Self)", tiedosto: tiedosto, rivi: rivi)
        XCTAssertEqual (virhe muodossa? E, odotettu virhe), tiedosto: tiedosto, rivi: rivi)
    })
}
public func XCTAssertThrowsError  (_ lauseke: @autoclosure () heittää -> T, odotettuErrorType: E.Type, _ viesti: String = "", tiedosto: StaticString = # tiedosto, rivi: UInt = # line ) {
    XCTAssertThrowsError (kokeile lauseketta (), viesti, tiedosto: tiedosto, rivi: rivi, {(virhe)
        XCTAssertNotNil (virhe nimellä? E, "\ (virhe) ei ole \ (E. Self)", tiedosto: tiedosto, rivi: rivi)
    })
}

Vaikka se ei heitä ...

Heitotoimintojen voimaa voidaan käyttää vahvempien testien kirjoittamiseen myös muille skenaarioille, joissa säännöllistä tasa-arvoa ei voida soveltaa.

Harkitse enumia, jota ei voida tehdä yhtäläiseksi - esimerkiksi jos sen tapausten liittyvät arvot eivät ole samanarvoisia.
Sen sijaan, että kytkintä suoritettaisiin testitapauksissa, kirjoitamme puhtaita “auttaja” toimintoja, jotka heittävät merkityksellisiä virheitä.

Yleinen esimerkki on tuloksen enum:

enum tulos {
    tapauksen menestys ([merkkijono: mikä tahansa])
    tapausvirhe (virhe)
}

Jos testaamme tulosta suoraan palauttavaa toimintoa, jouduimme vaihtamaan palautetun arvon ja kutsumaan XCTFail väärissä tapauksissa. Kopioimme ja liitämme kytkimen jokaiselle testitapaukselle, ja uusien enum-tapausten testien päivittäminen olisi painajainen.

Sen sijaan voimme luoda avustajien heittofunktioita hoitaa enumia yhdessä paikassa:

XCTAssert (kokeile tulemusta.assertIsSuccess (assertValue: {(arvo: [merkkijono: mikä tahansa]))
    XCTAssertEqual (arvo.luku, 10)
}))
XCTAssert (kokeile tulemusta.assertIsFailure (assertError: {(arvo: virhe))
    XCTAssertEquals (arvo, MyError.case)
}))
// MARK: Auttajat
yksityinen laajennus Tulos {
    private enum -virhe: Swift.Error, CustomStringConvertible {
        var viesti: merkkijono
        var kuvaus: merkkijono {paluuviesti}
    }
    func assertIsSuccess (assertValue: (([String: Any]) heittää -> Void)? = nolla) heittää -> Bool {
        vaihda itse {
        tapaus .menestys (anna arvo):
            kokeile assertValue? (arvo)
            palaa totta
        tapaus .vika (_):
            heittovirhe (viesti: "Odotettu .menestys, sai. \ (itse)")
        }
    }
    func assertIsFailure (assertError: ((Virhe) heittää -> Void)? = nolla) heittää -> Bool {
        vaihda itse {
        tapaus .menestys (_):
            heittovirhe (viesti: "Odotettu epäonnistuminen, sai. \ (itse)")
        case .failure (anna virhe):
            kokeile assertError? (arvo)
            palaa totta
        }
    }
}

Tällaista lähestymistapaa voidaan käyttää erilaisissa tilanteissa, kuten valinnaisten tarkistusten tarkastaminen (jotka ovat myös enumeja: troll :).

Muistiinpano mukautettujen vakuuttavien toimintojen luomisesta

Muutamia asioita on pidettävä mielessä kirjoitettaessa mukautettuja testitoimintoja.

  1. On hyvä käytäntö lisätä rivi- ja tiedostoargumentteja siirtämällä ne kaikki vakio XCTAssert -toimintoihin. Tällä tavalla testitapauksen viat ilmoitetaan siinä kohdassa, jossa mukautettuasi väitettäsi kutsutaan, ei itse toiminnon rungossa.
  2. On hyvä lisätä viestiparametri, jotta soittaja voi antaa kontekstin testiin. On myös hyvä käyttää niitä kirjoittaessasi testitapauksia :)
  3. XCTFail (viesti :) tarjoaa tavan testiä ehdottomasti epäonnistua, mikä voi olla erittäin hyödyllinen esimerkiksi kun testataan epätasaisia ​​enumeja ja joudutaan odottamattomaan tapaukseen.

*

XCTest-kehys on kasvanut erittäin tehokkaaksi viime vuonna, ja yritämme pysyä hyödyntämällä olemassa olevia toimintoja mahdollisimman paljon sen sijaan, että toisimme heidän käyttäytymistään.

On syytä huomata, että NSExmissionsille XCTest-kehys tarjoaa rikkaamman API: n, joka on valitettavasti käytettävissä vain Objective-C: ssä: https://developer.apple.com/documentation/xctest/nsexception_assertions?language=objc

Kaikkien vakuuttavien toimintojen täydellinen dokumentaatio löytyy täältä: https://developer.apple.com/documentation/xctest

Alun perin tämän inspiroima: http://ericasadun.com/2017/05/27/tests-that-dont-crash/

*

Päivitys Xcode 9: lle: uusia vakuuttavia sovellusliittymiä ei ole vielä lisätty. Odotan innolla tukea NSExceptions-sovelluksille XCTestin Swift-versiossa!

*

Päivitys Xcode 10.2: lle: uusia vakuuttavia sovellusliittymiä ei ole lisätty.

Olen alkanut tutkia, miten voin osallistua avoimen lähdekoodin toteutukseen ja luonut äänen Swift-foorumeille. Haluatko auttaa minua tässä? Ota yhteyttä minuun täällä tai Twitterissä. Tehdään yhdessä XCTest!

Kiitos lukemisesta! Jos nautit artikkelista, jaa se napsauttamalla artikkelin alla olevaa Jaa-painiketta - enemmän ihmiset hyötyvät siitä.

Voit seurata minua myös Twitterissä, jossa kirjoitan enimmäkseen iOS-kehityksestä.

Haluatko työskennellä kanssani? Storytel palkkaa - hae täältä