iOS Unit Testing and UI Testing Tutorial

Opdateret note: Michael Katz har opdateret denne vejledning til Xcode 10.1, Swift 4.2 og iOS 12. Audrey Tam skrev den oprindelige.

Det er ikke glamourøst at skrive tests, men da tests forhindrer din funklende app i at blive til et fejlbefængt stykke skrammel, er det nødvendigt. Hvis du læser denne vejledning, ved du allerede, at du bør skrive tests for din kode og brugergrænseflade, men du ved måske ikke hvordan.

Du har måske en fungerende app, men du vil gerne teste de ændringer, du foretager for at udvide appen. Måske har du allerede skrevet tests, men er ikke sikker på, om det er de rigtige tests. Eller du er begyndt at arbejde på en ny app og ønsker at teste undervejs.

Denne vejledning vil vise dig:

  • Hvordan du bruger Xcodes testnavigator til at teste en apps model og asynkrone metoder
  • Hvordan du simulerer interaktioner med biblioteks- eller systemobjekter ved hjælp af stubs og mocks
  • Hvordan du tester UI og ydeevne
  • Hvordan du bruger kodedækningsværktøjet

Undervejs, vil du opsamle noget af det ordforråd, der bruges af test-ninjaer.

Find ud af, hvad der skal testes

Hvor du skriver nogen tests, er det vigtigt at kende det grundlæggende. Hvad har du brug for at teste?

Hvis dit mål er at udvide en eksisterende app, bør du først skrive tests for enhver komponent, du planlægger at ændre.

Generelt bør testene dække:

  • Kernefunktionalitet: Modelklasser og -metoder og deres interaktioner med controlleren
  • De mest almindelige arbejdsgange i brugergrænsefladen
  • Grænsebetingelser
  • Bug fixes

Best Practices for Testing

Akronymet FIRST beskriver et kortfattet sæt kriterier for effektive enhedstests. Disse kriterier er:

  • Hurtig: Testene skal køre hurtigt.
  • Uafhængige/isolerede: Testene bør ikke dele tilstand med hinanden.
  • Gentagelige: Du bør opnå de samme resultater, hver gang du kører en test. Eksterne dataudbydere eller samtidighedsproblemer kan forårsage intermitterende fejl.
  • Selvvaliderende: Testene bør være fuldt automatiserede. Output bør være enten “bestået” eller “ikke bestået”, i stedet for at være afhængig af en programmørens fortolkning af en logfil.
  • Rettidig: Ideelt set bør testene skrives, før du skriver den produktionskode, de tester (Test-Driven Development).

Følger du FIRST-principperne, vil dine test forblive klare og nyttige i stedet for at blive til vejspærringer for din app.

Gå i gang

Start med at downloade projektmaterialet ved hjælp af knappen Download materialer øverst eller nederst i denne vejledning. Der er to separate startprojekter: BullsEye og HalfTunes.

  • BullsEye er baseret på en prøveapp i iOS Apprentice. Spillogikken er i BullsEyeGame-klassen, som du skal afprøve i løbet af denne vejledning.
  • HalfTunes er en opdateret version af prøveappen fra URLSession-vejledningen. Brugerne kan forespørge iTunes API’et efter sange og derefter downloade og afspille sanguddrag.

Unit Testing in Xcode

Testnavigatoren giver den nemmeste måde at arbejde med tests på; du bruger den til at oprette testmål og køre tests mod din app.

Opret et enhedstestmål

Åbn BullsEye-projektet, og tryk på Command-6 for at åbne Test-navigatoren.

Klik på knappen + i det nederste venstre hjørne, og vælg derefter Nyt enhedstestmål… i menuen:

Accepter standardnavnet, BullsEyeTests. Når testbundtet vises i Test-navigatoren, skal du klikke på for at åbne bundtet i editoren. Hvis bundtet ikke vises automatisk, skal du foretage fejlfinding ved at klikke på en af de andre navigatorer og derefter vende tilbage til Test-navigatoren.

Den standardskabelon importerer testrammen, XCTest, og definerer en BullsEyeTests underklasse af XCTestCase med setUp(), tearDown() og eksempeltestmetoder.

Der er tre måder at køre testene på:

  1. Produkt ▸ Test eller Kommando-U. Begge disse kører alle testklasser.
  2. Klik på pilknappen i Test-navigatoren.
  3. Klik på diamantknappen i rendestenen.

Du kan også køre en individuel testmetode ved at klikke på dens diamant, enten i Test-navigatoren eller i rendestenen.

Afprøv de forskellige måder at køre test på for at få en fornemmelse af, hvor lang tid det tager, og hvordan det ser ud. Prøvetestene gør ikke noget endnu, så de kører meget hurtigt!

Når alle testene lykkes, bliver diamanterne grønne og viser afkrydsningsmærker. Du kan klikke på den grå diamant i slutningen af testPerformanceExample() for at åbne Ydelsesresultatet:

Du har ikke brug for testPerformanceExample() eller testExample() til denne vejledning, så slet dem.

Brug af XCTAssert til at teste modeller

Først skal du bruge XCTAssert-funktioner til at teste en kernefunktion i BullsEye-modellen: BullsEyeGame beregner et BullsEyeGame objekt korrekt scoren for en runde?

I BullsEyeTests.swift skal du tilføje denne linje lige under import-erklæringen:

@testable import BullsEye

Dette giver enhedstesten adgang til de interne typer og funktioner i BullsEye.

Overst i BullsEyeTests-klassen tilføjes denne egenskab:

var sut: BullsEyeGame!

Dette skaber en pladsholder for en BullsEyeGame, som er System Under Test (SUT), eller det objekt, som denne testcase-klasse skal teste.

Næst erstatter du indholdet af setup() med dette:

super.setUp()sut = BullsEyeGame()sut.startNewGame()

Dette opretter et BullsEyeGame-objekt på klasseniveau, så alle testene i denne testklasse kan få adgang til SUT-objektets egenskaber og metoder.

Her kalder du også spillets startNewGame(), som initialiserer targetValue. Mange af testene vil bruge targetValue til at teste, at spillet beregner scoren korrekt.

Hvor du glemmer det, skal du frigive dit SUT-objekt i tearDown(). Udskift dets indhold med:

sut = nilsuper.tearDown()
Bemærk: Det er god praksis at oprette SUT’en i setUp() og frigive den i tearDown() for at sikre, at hver test starter med en ren tavle. Hvis du vil have mere diskussion, kan du læse Jon Reids indlæg om emnet.

Skrivning af din første test

Nu er du klar til at skrive din første test!

Føj følgende kode til slutningen af 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 testmetodes navn begynder altid med test, efterfulgt af en beskrivelse af, hvad den tester.

Det er god praksis at formatere testen i givet, hvornår og derefter afsnit:

  1. Givet: Her opstiller du alle nødvendige værdier. I dette eksempel opretter du en guess-værdi, så du kan angive, hvor meget den adskiller sig fra targetValue.
  2. Når: I dette afsnit udfører du den kode, der skal testes: Kald check(guess:).
  3. Derefter: Dette er det afsnit, hvor du bekræfter det forventede resultat med en meddelelse, der udskrives, hvis testen mislykkes. I dette tilfælde skal sut.scoreRound være lig med 95 (100 – 5).

Kør testen ved at klikke på diamant-ikonet i rendestenen eller i Test-navigatoren. Dette vil bygge og køre appen, og diamantikonet ændres til et grønt flueben!

Bemærk: Hvis du vil se en komplet liste over XCTestAssertions, skal du gå til Apples Assertions Listed by Category.

Debugging a Test

Der er indbygget en fejl i BullsEyeGame med vilje, og du skal øve dig på at finde den nu. For at se fejlen i aktion skal du oprette en test, der trækker 5 fra targetValue i det givne afsnit og lader alt andet forblive uændret.

Føj følgende test til:

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

Differencen mellem guess og targetValue er stadig 5, så scoren bør stadig være 95.

I navigatoren Breakpoint-navigatoren skal du tilføje et Breakpoint for testfejl. Dette stopper testkørslen, når en testmetode sender en fejlbekræftelse.

Kør din test, og den bør stoppe ved XCTAssertEqual-linjen med en testfejl.

Inspicer sut og guess i debug-konsollen:

guess er targetValue - 5, men scoreRound er 105, ikke 95!

For at undersøge det yderligere skal du bruge den normale debugging-proces: Sæt et breakpoint ved when-erklæringen og også et i BullsEyeGame.swift, inde i check(guess:), hvor den opretter difference. Kør derefter testen igen, og gå over let difference-anvisningen for at inspicere værdien af difference i appen:

Problemet er, at difference er negativ, så resultatet er 100 – (-5). For at rette dette skal du bruge den absolutte værdi af difference. I check(guess:) udkommenterer du den korrekte linje og sletter den forkerte.

Fjern de to breakpoints, og kør testen igen for at bekræfte, at den nu lykkes.

Brug af XCTestExpectation til test af asynkrone operationer

Nu har du lært at teste modeller og fejlfinding af testfejl, og det er tid til at gå videre til test af asynkron kode.

Åbn HalfTunes-projektet. Det bruger URLSession til at forespørge iTunes API’et og hente sangprøver. Antag, at du ønsker at ændre det til at bruge AlamoFire til netværksoperationer. For at se, om noget går i stykker, bør du skrive tests for netværksoperationerne og køre dem før og efter, at du har ændret koden.

URLSession metoder er asynkrone: De vender tilbage med det samme, men er først færdige med at køre senere. Hvis du vil teste asynkrone metoder, bruger du XCTestExpectation til at få din test til at vente på, at den asynkrone operation er færdig.

Asynkrone tests er normalt langsomme, så du bør holde dem adskilt fra dine hurtigere enhedstests.

Opret et nyt enhedstestmål med navnet HalfTunesSlowTests. Åbn HalfTunesSlowTests-klassen, og importer HalfTunes-appmodulet lige under den eksisterende import-angivelse:

@testable import HalfTunes

Alle test i denne klasse bruger standard URLSession til at sende anmodninger til Apples servere, så deklarer et sut-objekt, opret det i setUp(), og frigiv det i tearDown().

Forsæt indholdet af HalfTunesSlowTests-klassen med:

var sut: URLSession!override func setUp() { super.setUp() sut = URLSession(configuration: .default)}override func tearDown() { sut = nil super.tearDown()}

Næst skal du tilføje denne asynkrone test:

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

Denne test kontrollerer, at afsendelse af en gyldig forespørgsel til iTunes returnerer en 200-statuskode. Det meste af koden er den samme som den, du ville skrive i appen, med disse ekstra linjer:

  • expectation(description:): returnerer et XCTestExpectation objekt, gemt i promise. Parameteren description beskriver, hvad du forventer, der skal ske.
  • promise.fulfill(): Kald dette i lukningen af succesbetingelsen i den asynkrone metodes afslutningshåndtering for at markere, at forventningen er blevet opfyldt.
  • wait(for:timeout:): Holder testen kørende, indtil alle forventninger er opfyldt, eller indtil timeout-intervallet slutter, alt efter hvad der sker først.
  • Kør testen. Hvis du har forbindelse til internettet, bør testen tage ca. et sekund, før den lykkes, efter at appen er indlæst i simulatoren.

    Svigt hurtigt

    Svigt gør ondt, men det behøver ikke at tage evigheder.

    For at opleve svigt skal du blot slette ‘s’ fra “itunes” i URL’en:

    let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")

    Kør testen. Den fejler, men den tager hele timeout-intervallet! Det skyldes, at du antog, at anmodningen altid ville lykkes, og det er der, du kaldte promise.fulfill(). Da anmodningen mislykkedes, blev den først afsluttet, da timeouttiden udløb.

    Du kan forbedre dette og få testen til at mislykkes hurtigere ved at ændre antagelsen: I stedet for at vente på, at anmodningen lykkes, skal du kun vente, indtil den asynkrone metodes afslutningshåndtering påkaldes. Dette sker, så snart appen modtager et svar – enten OK eller fejl – fra serveren, som opfylder forventningen. Din test kan derefter kontrollere, om anmodningen lykkedes.

    For at se, hvordan dette fungerer, opretter du en ny test.

    Men først skal du rette den tidligere test ved at fortryde den ændring, du foretog i url.
    Føj derefter følgende test til din klasse:

    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 vigtigste forskel er, at det blot er ved at indtaste færdiggørelseshåndteringen, at forventningen opfyldes, og det tager kun ca. et sekund at gøre det. Hvis anmodningen mislykkes, mislykkes then-assertionerne.

    Kør testen. Det bør nu tage ca. et sekund at fejle. Den mislykkes, fordi anmodningen mislykkedes, ikke fordi testkørslen overskred timeout.

    Retter url, og kør derefter testen igen for at bekræfte, at den nu lykkes.

    Faking Objects and Interactions

    Asynkrone tests giver dig tillid til, at din kode genererer korrekt input til et asynkront API. Du vil måske også gerne teste, at din kode fungerer korrekt, når den modtager input fra en URLSession, eller at den korrekt opdaterer brugerens standarddatabase eller en iCloud-container.

    De fleste apps interagerer med system- eller biblioteksobjekter – objekter, som du ikke har kontrol over – og tests, der interagerer med disse objekter, kan være langsomme og ikke kunne gentages, hvilket er i strid med to af FIRST-principperne. I stedet kan du simulere interaktionerne ved at få input fra stubs eller ved at opdatere mock-objekter.

    Anvend fakery, når din kode har en afhængighed af et system- eller biblioteksobjekt. Det kan du gøre ved at oprette et falsk objekt til at spille den pågældende rolle og injicere dette falske objekt i din kode. Dependency Injection af Jon Reid beskriver flere måder at gøre dette på.

    Fake Input From Stub

    I denne test kontrollerer du, at app’ens updateSearchResults(_:) analyserer data hentet af sessionen korrekt ved at kontrollere, at searchResults.count er korrekt. SUT’en er visningscontrolleren, og du simulerer sessionen med stubs og nogle på forhånd downloadede data.

    Gå til Test-navigatoren, og tilføj et nyt enhedstestmål. Navngiv det HalfTunesFakeTests. Åbn HalfTunesFakeTests.swift og importer HalfTunes-appmodulet lige under import-erklæringen:

    @testable import HalfTunes

    Nu skal du erstatte indholdet af HalfTunesFakeTests-klassen med dette:

    var sut: SearchViewController!override func setUp() { super.setUp() sut = UIStoryboard(name: "Main", bundle: nil) .instantiateInitialViewController() as? SearchViewController}override func tearDown() { sut = nil super.tearDown()}

    Dette erklærer SUT’en, som er en SearchViewController, opretter den i setUp() og frigiver den i tearDown():

    Bemærk: SUT’en er visionscontrolleren, fordi HalfTunes har et massivt visionscontrollerproblem – alt arbejdet udføres i SearchViewController.swift. Hvis du flytter netværkskoden til et separat modul, vil det mindske dette problem og også gøre det nemmere at teste.

    Næst skal du bruge nogle eksempler på JSON-data, som din falske session vil levere til din test. Kun få elementer er nok, så for at begrænse dine downloadresultater i iTunes skal du tilføje &limit=3 til URL-strengen:

    https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

    Kopier denne URL, og indsæt den i en browser. Dette downloader en fil med navnet 1.txt, 1.txt.js eller lignende. Vis et eksempel på den for at bekræfte, at det er en JSON-fil, og omdøb den derefter til abbaData.json.

    Nu skal du gå tilbage til Xcode og gå til projektnavigatoren. Tilføj filen til gruppen HalfTunesFakeTests.

    Det HalfTunes-projektet indeholder den understøttende fil DHURLSessionMock.swift. Denne definerer en simpel protokol med navnet DHURLSession med metoder (stubs) til at oprette en dataopgave med enten en URL eller en URLRequest. Den definerer også URLSessionMock, som er i overensstemmelse med denne protokol med initialisatorer, der giver dig mulighed for at oprette et mock URLSession-objekt med dit valg af data, svar og fejl.

    For at opsætte fake’en skal du gå til HalfTunesFakeTests.swift og tilføje følgende i setUp(), efter den erklæring, der opretter SUT’en:

    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

    Dette opstiller de falske data og det falske svar og opretter det falske sessionobjekt. Endelig injiceres den falske session til sidst i appen som en egenskab af sut.

    Nu er du klar til at skrive den test, der kontrollerer, om det at kalde updateSearchResults(_:) analyserer de falske data. Tilføj følgende 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 skal stadig skrive denne test som en asynkron test, fordi stub’en foregiver at være en asynkron metode.

    Den når-assertion er, at searchResults er tom, før dataopgaven køres. Dette bør være sandt, fordi du har oprettet en helt ny SUT i setUp().

    Den falske data indeholder JSON for tre Track-objekter, så then-assertionen er, at visningscontrollerens searchResults-array indeholder tre elementer.

    Kør testen. Den burde lykkes ret hurtigt, fordi der ikke er nogen reel netværksforbindelse!

    Fake Update to Mock Object

    Den foregående test brugte en stub til at levere input fra et falsk objekt. Nu skal du bruge et mock-objekt til at teste, at din kode opdaterer UserDefaults korrekt.

    Åbn BullsEye-projektet igen. Appen har to spilstile: Brugeren bevæger enten skyderen for at matche målværdien eller gætter målværdien ud fra skyderens position. En segmenteret kontrol i nederste højre hjørne skifter spilstil og gemmer den i brugerens standardindstillinger.

    Din næste test skal kontrollere, at appen gemmer gameStyle-egenskaben korrekt.

    Klik på Ny enhedstestklasse i Test-navigatoren, og giv den navnet BullsEyeMockTests. Tilføj følgende under import-erklæringen:

    @testable import BullsEyeclass MockUserDefaults: UserDefaults { var gameStyleChanged = 0 override func set(_ value: Int, forKey defaultName: String) { if defaultName == "gameStyle" { gameStyleChanged += 1 } }}

    MockUserDefaults overstyrer set(_:forKey:) for at øge gameStyleChanged-flaget. Ofte vil du se lignende tests, der indstiller en Bool-variabel, men ved at inkremere en Int får du mere fleksibilitet – din test kan f.eks. kontrollere, at metoden kun kaldes én gang.

    Deklarér SUT og mock-objektet i BullsEyeMockTests:

    var sut: ViewController!var mockUserDefaults: MockUserDefaults!

    Næst erstatter du standard setUp() og tearDown() med dette:

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

    Dette opretter SUT og mock-objektet og injicerer mock-objektet som en egenskab i SUT’en.

    Nu skal du erstatte de to standardtestmetoder i skabelonen med dette:

    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-assertion er, at gameStyleChanged-flaget er 0, før testmetoden ændrer den segmenterede kontrol. Så hvis then-assertionen også er sand, betyder det, at set(_:forKey:) blev kaldt præcis én gang.

    Kør testen; den bør lykkes.

    UI-testning i Xcode

    MedUI-testning kan du teste interaktioner med brugergrænsefladen. UI-testning fungerer ved at finde en app’s UI-objekter med forespørgsler, syntetisere begivenheder og derefter sende begivenhederne til disse objekter. API’et giver dig mulighed for at undersøge et UI-objekts egenskaber og tilstand for at sammenligne dem med den forventede tilstand.

    I BullsEye-projektets Test-navigator skal du tilføje et nyt UI-testmål. Kontroller, at det mål, der skal testes, er BullsEye, og accepter derefter standardnavnet BullsEyeUITests.

    Åbn BullsEyeUITests.swift, og tilføj denne egenskab øverst i BullsEyeUITests-klassen:

    var app: XCUIApplication!

    I setUp() erstatter du angivelsen XCUIApplication().launch() med følgende:

    app = XCUIApplication()app.launch()

    Ændrer navnet på testExample() til testGameStyleSwitch().

    Åbn en ny linje i testGameStyleSwitch(), og klik på den røde knap Optag nederst i editorvinduet:

    Derved åbnes appen i simulatoren i en tilstand, der registrerer dine interaktioner som testkommandoer. Når appen er indlæst, skal du trykke på Slide-segmentet i spilstilkontakten og den øverste etiket. Klik derefter på Xcode Record-knappen for at stoppe optagelsen.

    Du har nu følgende tre linjer i testGameStyleSwitch():

    let app = XCUIApplication()app.buttons.tap()app.staticTexts.tap()

    Optageren har oprettet kode til at teste de samme handlinger, som du testede i appen. Send et tryk til skyderen og etiketten. Du skal bruge dem som grundlag for at oprette din egen UI-test.
    Hvis du ser andre udsagn, skal du bare slette dem.

    Den første linje duplikerer den egenskab, du oprettede i setUp(), så slet den linje. Du behøver ikke at trykke på noget endnu, så slet også .tap() i slutningen af linje 2 og 3. Åbn nu den lille menu ved siden af , og vælg segmentedControls.buttons.

    Det, du står tilbage med, bør være følgende:

    app.segmentedControls.buttonsapp.staticTexts

    Tryk på andre objekter for at lade optageren hjælpe dig med at finde den kode, du kan få adgang til i dine tests. Erstat nu disse linjer med denne kode for at oprette et givet afsnit:

    // givenlet slideButton = app.segmentedControls.buttonslet typeButton = app.segmentedControls.buttonslet slideLabel = app.staticTextslet typeLabel = app.staticTexts

    Nu, hvor du har navne til de to knapper i den segmenterede kontrol og de to mulige øverste etiketter, skal du tilføje følgende kode nedenfor:

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

    Dette kontrollerer, om den korrekte etiket findes, når du tap() på hver enkelt knap i den segmenterede kontrol. Kør testen – alle assertions bør lykkes.

    Performance Testing

    Fra Apples dokumentation: En ydelsestest tager en blok kode, som du vil evaluere, og kører den ti gange og indsamler den gennemsnitlige udførelsestid og standardafvigelsen for kørslerne. Gennemsnittet af disse individuelle målinger danner en værdi for testkørslen, som derefter kan sammenlignes med en baseline for at vurdere succes eller fiasko.

    Det er meget enkelt at skrive en ydelsestestest: Du skal blot placere den kode, du vil måle, i lukningen af measure().

    For at se dette i praksis skal du genåbne HalfTunes-projektet og i HalfTunesFakeTests.swift, tilføj følgende 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 testen, og klik derefter på ikonet, der vises ved siden af begyndelsen af den efterfølgende lukning af measure(), for at se statistikkerne.

    Klik på Set Baseline for at indstille et referencetidspunkt. Kør derefter ydelsestesten igen, og se resultatet – det kan være bedre eller dårligere end basislinjen. Med knappen Rediger kan du nulstille baseline til dette nye resultat.

    Baselines gemmes pr. enhedskonfiguration, så du kan have den samme test, der udføres på flere forskellige enheder, og få hver enkelt til at opretholde en anden baseline afhængig af den specifikke konfigurations processorhastighed, hukommelse osv.

    Hver gang du foretager ændringer i en app, der kan påvirke ydeevnen for den metode, der testes, skal du køre ydelsestesten igen for at se, hvordan den er sammenlignet med baseline.

    Kodedækning

    Værktøjet til kodedækning fortæller dig, hvilken app-kode der rent faktisk bliver kørt af dine tests, så du ved, hvilke dele af app-koden der (endnu) ikke er testet.

    For at aktivere kodedækning skal du redigere skemaets testhandling og markere afkrydsningsfeltet Indsamle dækning for under fanen Indstillinger:

    Kør alle test (Kommando-U), og åbn derefter rapportnavigatoren (Kommando-9). Vælg Dækning under det øverste punkt på denne liste:

    Klik på afsløringstrekanten for at se listen over funktioner og lukninger i SearchViewController.swift:

    Rul ned til updateSearchResults(_:) for at se, at dækningen er 87,9 %.

    Klik på pileknappen for denne funktion for at åbne kildefilen til funktionen. Når du kører musen hen over dækningsanmærkningerne i højre sidebar, fremhæves kodeafsnit grønt eller rødt:

    Dækningsanmærkningerne viser, hvor mange gange en test rammer hvert kodeafsnit; afsnit, der ikke blev kaldt, er fremhævet med rødt. Som man kan forvente, blev for-loop’en kørt 3 gange, men intet i fejlstierne blev udført.

    For at øge dækningen af denne funktion kan du duplikere abbaData.json og derefter redigere den, så den forårsager de forskellige fejl. Ændre f.eks. "results" til "result" for en test, der rammer print("Results key not found in dictionary").

    100% dækning?

    Hvor hårdt skal man stræbe efter 100% kodedækning? Google “100% unit test coverage”, og du vil finde en række argumenter for og imod dette, sammen med debat om selve definitionen af “100% dækning”. Argumenter imod siger, at de sidste 10-15% ikke er indsatsen værd. Argumenter for det siger, at de sidste 10-15% er de vigtigste, fordi de er så svære at teste. Google “hard to unit test bad design” for at finde overbevisende argumenter for, at kode, der ikke kan testes, er et tegn på dybere designproblemer.

    Hvor skal vi gå hen herfra?

    Du har nu nogle gode værktøjer, som du kan bruge til at skrive tests til dine projekter. Jeg håber, at denne iOS Unit Testing and UI Testing tutorial har givet dig selvtillid til at teste alle tingene!

    Du kan downloade den færdige version af projektet ved hjælp af knappen Download Materials øverst eller nederst i denne tutorial. Fortsæt med at udvikle dine færdigheder ved at tilføje yderligere test af dine egne.

    Her er nogle ressourcer til yderligere studier:

    • Der er flere WWDC-videoer om emnet testning. To gode fra WWDC17 er: Engineering for Testability og Testing Tips & Tricks.
    • Det næste skridt er automatisering: Continuous Integration og Continuous Delivery. Start med Apples Automating the Test Process with Xcode Server og xcodebuild og Wikipedias artikel om kontinuerlig levering, som trækker på ekspertise fra ThoughtWorks.
    • Hvis du allerede har en app, men endnu ikke har skrevet test til den, kan du måske læse Working Effectively with Legacy Code af Michael Feathers, for kode uden test er legacy code!
    • Jon Reid’s Quality Coding sample app archives er gode til at lære mere om testdreven udvikling.

    raywenderlich.com Weekly

    Raywenderlich.com-nyhedsbrevet er den nemmeste måde at holde sig opdateret på alt, hvad du har brug for at vide som mobiludvikler.

    Få en ugentlig oversigt over vores tutorials og kurser, og modtag et gratis dybdegående e-mail-kursus som en bonus!

    Gennemsnitlig bedømmelse

    4,7/5

    Føj en bedømmelse til dette indhold

    Log ind for at tilføje en bedømmelse

    90 bedømmelser

    Skriv et svar

    Din e-mailadresse vil ikke blive publiceret.