Att skriva tester är inte glamoröst, men eftersom tester hindrar din gnistrande app från att förvandlas till ett felbemängt skräp är det nödvändigt. Om du läser den här handledningen vet du redan att du bör skriva tester för din kod och ditt användargränssnitt, men du kanske inte vet hur.
Du kanske har en fungerande app, men du vill testa ändringar du gör för att utöka appen. Kanske har du redan skrivit tester, men är inte säker på om det är rätt tester. Eller så har du börjat arbeta med en ny app och vill testa allt eftersom.
Denna handledning kommer att visa dig:
- Hur du använder Xcodes testnavigator för att testa en apps modell och asynkrona metoder
- Hur du fejkar interaktioner med biblioteks- eller systemobjekt med hjälp av stubs och mocks
- Hur du testar användargränssnittet och prestanda
- Hur du använder kodtäckningsverktyget
Under vägen, kommer du att lära dig en del av den vokabulär som används av testninjor.
För att ta reda på vad som ska testas
För att skriva några tester är det viktigt att känna till grunderna. Vad behöver du testa?
Om ditt mål är att utöka en befintlig app bör du först skriva tester för alla komponenter som du planerar att ändra.
Generellt sett bör testerna täcka:
- Kärnfunktionalitet: Modellklasser och metoder och deras interaktioner med kontrollern
- De vanligaste arbetsflödena i användargränssnittet
- Gränsvillkor
- Buggfixar
Bästa metoder för testning
Akronymen FIRST beskriver en kortfattad uppsättning kriterier för effektiva enhetstester. Dessa kriterier är:
- Snabbt: Testerna ska köras snabbt.
- Oberoende/isolerade: Testerna ska inte dela tillstånd med varandra.
- Repeterbart: Du bör få samma resultat varje gång du kör ett test. Externa dataleverantörer eller samtidighetsproblem kan orsaka intermittenta fel.
- Självvaliderande: Testerna bör vara helt automatiserade. Resultatet bör vara antingen ”godkänt” eller ”underkänt”, snarare än att förlita sig på en programmerares tolkning av en loggfil.
- I rätt tid: Om du följer FIRST-principerna kommer dina tester att vara tydliga och användbara, i stället för att förvandlas till vägspärrar för din app.
För att komma igång
Starta med att ladda ner projektmaterialet med hjälp av knappen Ladda ner material högst upp eller längst ner i den här handledningen. Det finns två separata startprojekt: BullsEye och HalfTunes.
- BullsEye är baserat på en exempelapp i iOS Apprentice. Spellogiken finns i klassen
BullsEyeGame
, som du kommer att testa under den här handledningen. - HalfTunes är en uppdaterad version av exempelappen från URLSession-handledningen. Användare kan fråga iTunes API efter låtar och sedan ladda ner och spela upp låtutdrag.
Unit Testing in Xcode
Navigatorn Test ger det enklaste sättet att arbeta med tester; du kommer att använda den för att skapa testmål och köra tester mot din app.
Skapa ett enhetstestmål
Öppna BullsEye-projektet och tryck på Command-6 för att öppna Testnavigatorn.
Klicka på +-knappen i det nedre vänstra hörnet och välj sedan New Unit Test Target… från menyn:
Acceptera standardnamnet, BullsEyeTests. När testpaketet visas i Testnavigatorn klickar du på för att öppna paketet i redigeraren. Om paketet inte visas automatiskt kan du felsöka genom att klicka på en av de andra navigatorerna och sedan återgå till testnavigatorn.
Standardmallen importerar testramverket XCTest och definierar en
BullsEyeTests
underklass tillXCTestCase
medsetUp()
,tearDown()
och exempel på testmetoder.Det finns tre sätt att köra testerna:
- Product ▸ Test eller Command-U. Båda dessa kör alla testklasser.
- Klicka på pilknappen i testnavigatorn.
- Klicka på diamantknappen i rännan.
Du kan också köra en enskild testmetod genom att klicka på dess diamant, antingen i Testnavigatorn eller i rännan.
Prova de olika sätten att köra tester för att få en känsla för hur lång tid det tar och hur det ser ut. Provtesterna gör ingenting ännu, så de körs väldigt snabbt!
När alla tester lyckas blir diamanterna gröna och visar kryssmarkeringar. Du kan klicka på den grå diamanten i slutet av
testPerformanceExample()
för att öppna prestandaresultatet:Du behöver inte
testPerformanceExample()
ellertestExample()
för den här handledningen, så ta bort dem.Användning av XCTAssert för att testa modeller
Först kommer du att använda
XCTAssert
-funktioner för att testa en kärnfunktion i BullsEyes modell:BullsEyeGame
beräknar ettBullsEyeGame
objekt korrekt poängen för en runda?I BullsEyeTests.swift lägger du till den här raden precis under
import
-angivelsen:@testable import BullsEye
Detta ger enhetstesterna tillgång till de interna typerna och funktionerna i BullsEye.
Toppst i
BullsEyeTests
-klassen lägger du till den här egenskapen:var sut: BullsEyeGame!
Detta skapar en platshållare för en
BullsEyeGame
, vilket är System Under Test (SUT), eller det objekt som den här testfallsklassen är intresserad av att testa.Nästan ersätter du innehållet i
setup()
med detta:super.setUp()sut = BullsEyeGame()sut.startNewGame()
Detta skapar ett
BullsEyeGame
-objekt på klassnivå, så att alla tester i denna testklass kan få tillgång till SUT-objektets egenskaper och metoder.Här anropar du också spelets
startNewGame()
, som initialiserartargetValue
. Många av testerna kommer att användatargetValue
för att testa att spelet beräknar poängen korrekt.För att du inte glömmer det släpper du ditt SUT-objekt i
tearDown()
. Ersätt dess innehåll med:sut = nilsuper.tearDown()
Obs: Det är bra att skapa SUT isetUp()
och släppa det itearDown()
för att se till att varje test börjar med ett rent blad. Mer diskussion finns i Jon Reids inlägg i ämnet.Skrivning av ditt första test
Nu är du redo att skriva ditt första test!
Lägg till följande kod i slutet av
BullsEyeTests
:func testScoreIsComputed() { // 1. given let guess = sut.targetValue + 5 // 2. when sut.check(guess: guess) // 3. then XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")}
En testmetods namn börjar alltid med test, följt av en beskrivning av vad den testar.
Det är en bra metod att formatera testet i given-, when- och then-avdelningar:
- Given: Här ställer du in alla värden som behövs. I det här exemplet skapar du ett
guess
-värde så att du kan ange hur mycket det skiljer sig fråntargetValue
. - När: I det här avsnittet utför du koden som ska testas: Kalla
check(guess:)
. - Då: Detta är avsnittet där du bekräftar det förväntade resultatet med ett meddelande som skrivs ut om testet misslyckas. I det här fallet ska
sut.scoreRound
vara lika med 95 (100 – 5).
Kör testet genom att klicka på diamantikonen i rännan eller i testnavigatorn. Detta kommer att bygga och köra appen, och diamantikonen kommer att ändras till ett grönt kryss!
Observera: Om du vill se en fullständig lista över XCTestAssertions kan du gå till Apples Assertions Listed by Category.Debugging a Test
Det finns ett fel som är inbyggt i
BullsEyeGame
med flit, och du ska öva på att hitta det nu. För att se hur felet fungerar ska du skapa ett test som subtraherar 5 fråntargetValue
i det givna avsnittet och lämnar allt annat oförändrat.Lägg till följande test:
func testScoreIsComputedWhenGuessLTTarget() { // 1. given let guess = sut.targetValue - 5 // 2. when sut.check(guess: guess) // 3. then XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")}
Differensen mellan
guess
ochtargetValue
är fortfarande 5, så poängen bör fortfarande vara 95.I navigatorn för brytpunkter lägger du till en brytpunkt för testfel. Detta kommer att stoppa testkörningen när en testmetod lägger upp ett felmeddelande.
Kör testet och det bör stanna på
XCTAssertEqual
-linjen med ett testfel.Inspektera
sut
ochguess
i felsökningskonsolen:guess
ärtargetValue - 5
menscoreRound
är 105, inte 95!Om du vill undersöka vidare använder du den normala felsökningsprocessen: Sätt en brytpunkt vid when-anvisningen och även en i BullsEyeGame.swift, inuti
check(guess:)
, där den skapardifference
. Kör sedan testet igen, och ta ett steg överlet difference
-anvisningen för att inspektera värdet pådifference
i appen:Problemet är att
difference
är negativ, så poängen är 100 – (-5). För att åtgärda detta bör du använda det absoluta värdet avdifference
. Icheck(guess:)
tar du bort den korrekta raden och raderar den felaktiga.Ta bort de två brytpunkterna och kör testet igen för att bekräfta att det nu lyckas.
Användning av XCTestExpectation för att testa asynkrona operationer
Nu när du lärt dig hur man testar modeller och felsöker testmisslyckanden är det dags att gå vidare med testning av asynkron kod.
Öppna HalfTunes-projektet. Det använder
URLSession
för att fråga efter iTunes API och ladda ner låtprover. Antag att du vill ändra det så att det använder AlamoFire för nätverksoperationer. För att se om något går sönder bör du skriva tester för nätverksoperationerna och köra dem före och efter att du har ändrat koden.URLSession
Metoder är asynkrona: De returnerar genast, men slutar inte köra förrän senare. För att testa asynkrona metoder använder duXCTestExpectation
för att få ditt test att vänta på att den asynkrona operationen ska slutföras.Asynkrona tester är vanligtvis långsamma, så du bör hålla dem åtskilda från dina snabbare enhetstester.
Skapa ett nytt enhetstestmål med namnet HalfTunesSlowTests. Öppna
HalfTunesSlowTests
-klassen och importera HalfTunes-appmodulen precis under det befintligaimport
-anvisningen:@testable import HalfTunes
Alla tester i den här klassen använder standard
URLSession
för att skicka förfrågningar till Apples servrar, så deklarera ettsut
-objekt, skapa det isetUp()
och släpp det itearDown()
.Ersätt innehållet i
HalfTunesSlowTests
-klassen med:var sut: URLSession!override func setUp() { super.setUp() sut = URLSession(configuration: .default)}override func tearDown() { sut = nil super.tearDown()}
Nästan lägger du till det här asynkrona testet:
// Asynchronous test: success fast, failure slowfunc testValidCallToiTunesGetsHTTPStatusCode200() { // given let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") // 1 let promise = expectation(description: "Status code: 200") // when let dataTask = sut.dataTask(with: url!) { data, response, error in // then if let error = error { XCTFail("Error: \(error.localizedDescription)") return } else if let statusCode = (response as? HTTPURLResponse)?.statusCode { if statusCode == 200 { // 2 promise.fulfill() } else { XCTFail("Status code: \(statusCode)") } } } dataTask.resume() // 3 wait(for: , timeout: 5)}
Det här testet kontrollerar att sändning av en giltig förfrågan till iTunes returnerar en 200-statuskod. Det mesta av koden är samma som du skulle skriva i appen, med dessa ytterligare rader:
- expectation(description:): Returnerar ett
XCTestExpectation
objekt, lagrat ipromise
. Parameterndescription
beskriver vad du förväntar dig ska hända. - promise.fulfill(): Kalla det här i stängningen av framgångsvillkoret i den asynkrona metodens avslutningshanterare för att flagga för att förväntningen har uppfyllts.
- wait(for:timeout:): Håller testet igång tills alla förväntningar är uppfyllda eller tills
timeout
intervallet slutar, beroende på vilket som inträffar först.
Kör testet. Om du är ansluten till internet bör testet ta ungefär en sekund innan det lyckas efter att appen laddas i simulatorn.
Sviker snabbt
Fel gör ont, men det behöver inte ta en evighet.
För att uppleva ett misslyckande tar du helt enkelt bort ’s’ från ”itunes” i webbadressen:
let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
Kör testet. Det misslyckas, men det tar hela timeoutintervallet! Detta beror på att du antog att begäran alltid skulle lyckas, och det var därför du kallade
promise.fulfill()
. Eftersom begäran misslyckades avslutades den först när timeouttiden gick ut.Du kan förbättra detta och få testet att misslyckas snabbare genom att ändra antagandet: Istället för att vänta på att begäran ska lyckas, vänta bara tills den asynkrona metodens avslutningshanterare anropas. Detta sker så snart appen får ett svar – antingen OK eller fel – från servern som uppfyller förväntningarna. Ditt test kan sedan kontrollera om begäran lyckades.
För att se hur detta fungerar skapar du ett nytt test.
Men först fixar du det föregående testet genom att ångra ändringen du gjorde i
url
.
När du sedan lägger till följande test i din klass:func testCallToiTunesCompletes() { // given let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba") let promise = expectation(description: "Completion handler invoked") var statusCode: Int? var responseError: Error? // when let dataTask = sut.dataTask(with: url!) { data, response, error in statusCode = (response as? HTTPURLResponse)?.statusCode responseError = error promise.fulfill() } dataTask.resume() wait(for: , timeout: 5) // then XCTAssertNil(responseError) XCTAssertEqual(statusCode, 200)}
Den viktigaste skillnaden är att det räcker med att skriva in avslutningshanteraren för att uppfylla förväntningen, och det tar bara ungefär en sekund att göra det. Om begäran misslyckas misslyckas
then
försäkran.Kör testet. Det bör nu ta ungefär en sekund innan det misslyckas. Det misslyckas för att begäran misslyckades, inte för att testkörningen överskred
timeout
.Rätta
url
och kör sedan testet igen för att bekräfta att det nu lyckas.Faking Objects and Interactions
Asynkrona tester ger dig förtroende för att din kod genererar korrekt indata till ett asynkront API. Du kanske också vill testa att din kod fungerar korrekt när den tar emot indata från en
URLSession
, eller att den uppdaterar användarens standarddatabas eller en iCloud-behållare på rätt sätt.De flesta appar interagerar med system- eller biblioteksobjekt – objekt som du inte kontrollerar – och tester som interagerar med dessa objekt kan vara långsamma och omöjliga att upprepa, vilket bryter mot två av FIRST-principerna. Istället kan du fejka interaktionerna genom att få input från stubs eller genom att uppdatera mock-objekt.
Använd fejkning när din kod har ett beroende av ett system- eller biblioteksobjekt. Du kan göra detta genom att skapa ett falskt objekt som spelar den rollen och injicera detta falska objekt i din kod. Dependency Injection av Jon Reid beskriver flera sätt att göra detta.
Fake Input From Stub
I det här testet kontrollerar du att appens
updateSearchResults(_:)
analyserar data som hämtas av sessionen korrekt genom att kontrollera attsearchResults.count
är korrekt. SUT är visningskontrollanten, och du fejkar sessionen med stubs och några nedladdade data i förväg.Gå till testnavigatorn och lägg till ett nytt enhetstestmål. Ge det namnet HalfTunesFakeTests. Öppna HalfTunesFakeTests.swift och importera HalfTunes-appmodulen precis under
import
-angivelsen:@testable import HalfTunes
Ersätt nu innehållet i
HalfTunesFakeTests
-klassen med detta:var sut: SearchViewController!override func setUp() { super.setUp() sut = UIStoryboard(name: "Main", bundle: nil) .instantiateInitialViewController() as? SearchViewController}override func tearDown() { sut = nil super.tearDown()}
Detta deklarerar SUT, som är en
SearchViewController
, skapar den isetUp()
och släpper den itearDown()
:Observera: SUT är visningskontrollanten, eftersom HalfTunes har ett stort problem med visningskontrollanter – allt arbete görs i SearchViewController.swift. Att flytta nätverkskoden till en separat modul skulle minska detta problem och även göra testningen enklare.Nästan behöver du några exempel på JSON-data som din fejkade session kommer att ge till ditt test. Det räcker med några få objekt, så för att begränsa dina nedladdningsresultat i iTunes lägger du till
&limit=3
till URL-strängen:https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3
Kopiera den här URL:n och klistra in den i en webbläsare. Detta laddar ner en fil som heter 1.txt, 1.txt.js eller liknande. Förhandsgranska den för att bekräfta att det är en JSON-fil och döpa sedan om den till abbaData.json.
Nu går du tillbaka till Xcode och går till projektnavigatorn. Lägg till filen i gruppen HalfTunesFakeTests.
HelfTunes-projektet innehåller stödfilen DHURLSessionMock.swift. Denna definierar ett enkelt protokoll med namnet
DHURLSession
, med metoder (stubs) för att skapa en datauppgift med antingen enURL
eller enURLRequest
. Den definierar ocksåURLSessionMock
, som överensstämmer med detta protokoll med initialiserare som låter dig skapa ett mockURLSession
-objekt med ditt val av data, svar och fel.För att konfigurera fejken går du till HalfTunesFakeTests.swift och lägger till följande i
setUp()
, efter det uttalande som skapar SUT:let testBundle = Bundle(for: type(of: self))let path = testBundle.path(forResource: "abbaData", ofType: "json")let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")let urlResponse = HTTPURLResponse( url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)sut.defaultSession = sessionMock
Detta skapar falska data och svar och skapar det falska sessionsobjektet. Slutligen injiceras den falska sessionen i appen som en egenskap hos
sut
.Nu är du redo att skriva testet som kontrollerar om anropande av
updateSearchResults(_:)
analyserar de falska uppgifterna. Lägg till följande test:func test_UpdateSearchResults_ParsesData() { // given let promise = expectation(description: "Status code: 200") // when XCTAssertEqual( sut.searchResults.count, 0, "searchResults should be empty before the data task runs") let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let dataTask = sut.defaultSession.dataTask(with: url!) { data, response, error in // if HTTP request is successful, call updateSearchResults(_:) // which parses the response data into Tracks if let error = error { print(error.localizedDescription) } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { self.sut.updateSearchResults(data) } promise.fulfill() } dataTask.resume() wait(for: , timeout: 5) // then XCTAssertEqual(sut.searchResults.count, 3, "Didn't parse 3 items from fake response")}
Du måste fortfarande skriva det här som ett asynkront test eftersom stubben låtsas vara en asynkron metod.
Den när-försäkran är att
searchResults
är tom innan datauppgiften körs. Detta bör vara sant eftersom du skapade ett helt nytt SUT isetUp()
.Den falska datan innehåller JSON för tre
Track
-objekt, så then-försäkran är att view controllernssearchResults
-array innehåller tre objekt.Kör testet. Det borde lyckas ganska snabbt eftersom det inte finns någon riktig nätverksanslutning!
Fake Update to Mock Object
Det föregående testet använde en stub för att ge indata från ett falskt objekt. Nu ska du använda ett låtsasobjekt för att testa att din kod uppdaterar
UserDefaults
korrekt.Öppna BullsEye-projektet igen. Appen har två spelstilar: Användaren flyttar antingen skjutreglaget för att matcha målvärdet eller gissar målvärdet utifrån skjutreglagets position. En segmenterad kontroll i det nedre högra hörnet växlar spelstilen och sparar den i användarens standardinställningar.
Ditt nästa test kommer att kontrollera att appen sparar egenskapen
gameStyle
på rätt sätt.I testnavigatorn klickar du på New Unit Test Class (Ny klass för enhetstest) och ger den namnet BullsEyeMockTests. Lägg till följande under
import
-angivelsen:@testable import BullsEyeclass MockUserDefaults: UserDefaults { var gameStyleChanged = 0 override func set(_ value: Int, forKey defaultName: String) { if defaultName == "gameStyle" { gameStyleChanged += 1 } }}
MockUserDefaults
åsidosätterset(_:forKey:)
för att ökagameStyleChanged
-flaggan. Ofta ser du liknande tester som ställer in enBool
-variabel, men att inkrementera enInt
ger dig mer flexibilitet – ditt test kan till exempel kontrollera att metoden endast anropas en gång.Deklarera SUT och mockobjektet i
BullsEyeMockTests
:var sut: ViewController!var mockUserDefaults: MockUserDefaults!
Nästan ersätter du standardvärdena
setUp()
ochtearDown()
med detta:override func setUp() { super.setUp() sut = UIStoryboard(name: "Main", bundle: nil) .instantiateInitialViewController() as? ViewController mockUserDefaults = MockUserDefaults(suiteName: "testing") sut.defaults = mockUserDefaults}override func tearDown() { sut = nil mockUserDefaults = nil super.tearDown()}
Detta skapar SUT och mockobjektet och injicerar mockobjektet som en egenskap hos SUT.
Ersätt nu de två standardtestmetoderna i mallen med detta:
func testGameStyleCanBeChanged() { // given let segmentedControl = UISegmentedControl() // when XCTAssertEqual( mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions") segmentedControl.addTarget(sut, action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged) segmentedControl.sendActions(for: .valueChanged) // then XCTAssertEqual( mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")}
Den när-försäkran är att
gameStyleChanged
-flaggan är 0 innan testmetoden ändrar den segmenterade kontrollen. Så om då-försäkran också är sann betyder det attset(_:forKey:)
anropades exakt en gång.Kör testet; det bör lyckas.
UI-testning i Xcode
Med hjälp avUI-testning kan du testa interaktioner med användargränssnittet. UI-testning fungerar genom att hitta en apps UI-objekt med hjälp av förfrågningar, syntetisera händelser och sedan skicka händelserna till dessa objekt. API:et gör det möjligt att undersöka ett UI-objekts egenskaper och tillstånd för att jämföra dem med det förväntade tillståndet.
I BullsEye-projektets testnavigator lägger du till ett nytt UI-testmål. Kontrollera att det mål som ska testas är BullsEye och acceptera sedan standardnamnet BullsEyeUITests.
Öppna BullsEyeUITests.swift och lägg till den här egenskapen högst upp i klassen
BullsEyeUITests
:var app: XCUIApplication!
I
setUp()
byter du ut påståendetXCUIApplication().launch()
mot följande:app = XCUIApplication()app.launch()
Ändra namnet på
testExample()
tilltestGameStyleSwitch()
.Öppna en ny rad i
testGameStyleSwitch()
och klicka på den röda knappen Spela in längst ner i redigeringsfönstret:Detta öppnar appen i simulatorn i ett läge som registrerar dina interaktioner som testkommandon. När appen laddas trycker du på Slide-segmentet för spelstilsomkopplaren och den övre etiketten. Klicka sedan på knappen Xcode Record för att stoppa inspelningen.
Du har nu följande tre rader i
testGameStyleSwitch()
:let app = XCUIApplication()app.buttons.tap()app.staticTexts.tap()
Inspelaren har skapat kod för att testa samma åtgärder som du testade i appen. Skicka ett tryck på reglaget och etiketten. Du kommer att använda dessa som en bas för att skapa ditt eget UI-test.
Om du ser några andra uttalanden är det bara att radera dem.Den första raden dubblar egenskapen som du skapade i
setUp()
, så radera den raden. Du behöver inte trycka på något ännu, så radera också.tap()
i slutet av rad 2 och 3. Öppna nu den lilla menyn bredvidoch välj
segmentedControls.buttons
.Det du har kvar bör vara följande:
app.segmentedControls.buttonsapp.staticTexts
Tappa på andra objekt för att låta inspelaren hjälpa dig att hitta koden som du kan komma åt i dina tester. Ersätt nu dessa rader med den här koden för att skapa ett givet avsnitt:
// givenlet slideButton = app.segmentedControls.buttonslet typeButton = app.segmentedControls.buttonslet slideLabel = app.staticTextslet typeLabel = app.staticTexts
När du nu har namn på de två knapparna i den segmenterade kontrollen och de två möjliga övre etiketterna lägger du till följande kod nedan:
// thenif slideButton.isSelected { XCTAssertTrue(slideLabel.exists) XCTAssertFalse(typeLabel.exists) typeButton.tap() XCTAssertTrue(typeLabel.exists) XCTAssertFalse(slideLabel.exists)} else if typeButton.isSelected { XCTAssertTrue(typeLabel.exists) XCTAssertFalse(slideLabel.exists) slideButton.tap() XCTAssertTrue(slideLabel.exists) XCTAssertFalse(typeLabel.exists)}
Detta kontrollerar om rätt etikett finns när du
tap()
på varje knapp i den segmenterade kontrollen. Kör testet – alla påståenden bör lyckas.Performance Testing
Från Apples dokumentation: En prestandatester tar ett kodblock som du vill utvärdera och kör det tio gånger och samlar in den genomsnittliga exekveringstiden och standardavvikelsen för körningarna. Genomsnittet av dessa individuella mätningar bildar ett värde för testkörningen som sedan kan jämföras med en baslinje för att utvärdera framgång eller misslyckande.
Det är mycket enkelt att skriva ett prestandatest: Du placerar bara koden som du vill mäta i avslutningen av
measure()
.För att se detta i praktiken öppnar du HalfTunes-projektet igen och i HalfTunesFakeTests.swift, lägg till följande test:
func test_StartDownload_Performance() { let track = Track( name: "Waterloo", artist: "ABBA", previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a") measure { self.sut.startDownload(track) }}
Kör testet och klicka sedan på ikonen som visas bredvid början av den avslutande
measure()
för att se statistiken.Klicka på Set Baseline (Ställ in baslinje) för att ställa in en referenstid. Kör sedan prestandatestet igen och se resultatet – det kan vara bättre eller sämre än referenslinjen. Med knappen Redigera kan du återställa baslinjen till det nya resultatet.
Baslinjer lagras per enhetskonfiguration, så du kan ha samma test som körs på flera olika enheter och låta varje enhet behålla en annan baslinje beroende på den specifika konfigurationens processorhastighet, minne etc.
Varje gång du gör ändringar i en app som kan påverka prestandan för den metod som testas kör du prestandatestet igen för att se hur det förhåller sig till baslinjen.
Kodtäckning
Verktyget för kodtäckning talar om för dig vilken appkod som faktiskt körs av dina tester, så att du vet vilka delar av appkoden som (ännu) inte är testade.
För att aktivera kodtäckning redigerar du schemats åtgärd Test och markerar kryssrutan Samla täckning för under fliken Alternativ:
Kör alla tester (Kommando-U) och öppna sedan rapportnavigatorn (Kommando-9). Välj Täckning under det översta objektet i den listan:
Klicka på avslöjandetriangeln för att se listan över funktioner och closures i SearchViewController.swift:
Rulla ner till
updateSearchResults(_:)
för att se att täckningsgraden är 87,9 %.Klicka på pilknappen för den här funktionen för att öppna källfilen till funktionen. När du går med musen över täckningsanmärkningarna i den högra sidofältet markeras kodsektioner grönt eller rött:
Täckningsanmärkningarna visar hur många gånger ett test träffar varje kodsektion; sektioner som inte anropades är markerade med rött. Som man kan förvänta sig kördes for-slingan tre gånger, men ingenting i felvägarna utfördes.
För att öka täckningen av den här funktionen kan du duplicera abbaData.json och sedan redigera den så att den orsakar de olika felen. Ändra till exempel
"results"
till"result"
för ett test som träffarprint("Results key not found in dictionary")
.100% täckning?
Hur hårt ska man sträva efter 100% kodtäckning? Googla ”100 % täckning av enhetstester” och du kommer att hitta en rad argument för och emot detta, tillsammans med en debatt om själva definitionen av ”100 % täckning”. Argumenten mot det säger att de sista 10-15 procenten inte är värda ansträngningen. Argumenten för det säger att de sista 10-15 procenten är de viktigaste, eftersom de är så svåra att testa. Googla ”hard to unit test bad design” för att hitta övertygande argument för att otestbar kod är ett tecken på djupare designproblem.
Var ska vi ta vägen härifrån?
Du har nu några bra verktyg som du kan använda när du skriver tester för dina projekt. Jag hoppas att denna iOS Unit Testing and UI Testing-tutorial har gett dig självförtroende att testa allt!
Du kan ladda ner den färdiga versionen av projektet med hjälp av knappen Ladda ner material högst upp eller längst ner i den här tutorialen. Fortsätt att utveckla dina färdigheter genom att lägga till ytterligare egna tester.
Här är några resurser för vidare studier:
- Det finns flera WWDC-videor om ämnet testning. Två bra videor från WWDC17 är: Engineering for Testability och Testing Tips & Tricks.
- Nästa steg är automatisering: Continuous Integration och Continuous Delivery. Börja med Apples Automating the Test Process with Xcode Server och
xcodebuild
och Wikipedias artikel om kontinuerlig leverans, som bygger på expertis från ThoughtWorks. - Om du redan har en app men inte har skrivit tester för den ännu, kan du läsa Working Effectively with Legacy Code av Michael Feathers, eftersom kod utan tester är legacy code!
- Jon Reids arkiv med exempelappar för Quality Coding är bra för att lära dig mer om testdriven utveckling.
raywenderlich.com Weekly
Nyhetsbrevet raywenderlich.com är det enklaste sättet att hålla sig uppdaterad om allt du behöver veta som mobilutvecklare.
Få en veckovis sammanfattning av våra handledningar och kurser och få en kostnadsfri fördjupad e-postkurs som en bonus!
Genomsnittligt betyg
4,7/5
Lägg till ett betyg för detta innehåll
Logga in för att lägga till ett betyg
90 betyg - BullsEye är baserat på en exempelapp i iOS Apprentice. Spellogiken finns i klassen