iOS Unit Testing e UI Testing Tutorial

Nota di aggiornamento: Michael Katz ha aggiornato questo tutorial per Xcode 10.1, Swift 4.2 e iOS 12. Audrey Tam ha scritto l’originale.

Scrivere test non è affascinante, ma dal momento che i test impediscono alla vostra app frizzante di trasformarsi in un pezzo di spazzatura pieno di bug, è necessario. Se stai leggendo questo tutorial, sai già che dovresti scrivere dei test per il tuo codice e la tua UI, ma forse non sai come farlo.

Potresti avere un’app funzionante, ma vuoi testare i cambiamenti che stai facendo per estendere l’app. Forse avete già scritto dei test, ma non siete sicuri che siano i test giusti. Oppure, hai iniziato a lavorare su una nuova app e vuoi testare man mano che vai avanti.

Questo tutorial te lo mostrerà:

  • Come usare il Test navigator di Xcode per testare il modello di un’app e i metodi asincroni
  • Come simulare interazioni con oggetti di libreria o di sistema usando stub e mock
  • Come testare l’UI e le prestazioni
  • Come usare lo strumento di copertura del codice

Per tutto il tempo, imparerai un po’ del vocabolario usato dai ninja del testing.

Capire cosa testare

Prima di scrivere qualsiasi test, è importante conoscere le basi. Cosa devi testare?

Se il tuo obiettivo è quello di estendere un’applicazione esistente, dovresti prima scrivere dei test per ogni componente che hai intenzione di cambiare.

Generalmente, i test dovrebbero coprire:

  • Funzionalità di base: Le classi e i metodi del modello e le loro interazioni con il controllore
  • I flussi di lavoro più comuni dell’UI
  • Le condizioni limite
  • Le correzioni dei bug

Le migliori pratiche per i test

L’acronimo FIRST descrive un insieme conciso di criteri per test unitari efficaci. Questi criteri sono:

  • Veloce: I test dovrebbero essere eseguiti velocemente.
  • Indipendenti/Isolati: I test non dovrebbero condividere lo stato tra loro.
  • Ripetibile: Si dovrebbero ottenere gli stessi risultati ogni volta che si esegue un test. Fornitori di dati esterni o problemi di concorrenza potrebbero causare fallimenti intermittenti.
  • Autovalidazione: I test dovrebbero essere completamente automatizzati. L’output dovrebbe essere “pass” o “fail”, piuttosto che basarsi sull’interpretazione di un file di log da parte di un programmatore.
  • Tempestivo: Idealmente, i test dovrebbero essere scritti prima di scrivere il codice di produzione che testano (Test-Driven Development).

Seguendo i principi di FIRST manterrai i tuoi test chiari e utili, invece di trasformarsi in blocchi stradali per la tua applicazione.

Inizio

Inizia scaricando i materiali del progetto usando il pulsante Download Materials in cima o in fondo a questo tutorial. Ci sono due progetti di partenza separati: BullsEye e HalfTunes.

  • BullsEye è basato su un’applicazione campione in iOS Apprentice. La logica di gioco è nella classe BullsEyeGame, che testerete durante questo tutorial.
  • HalfTunes è una versione aggiornata dell’applicazione campione del tutorial URLSession. Gli utenti possono interrogare l’API di iTunes per le canzoni, quindi scaricare e riprodurre frammenti di canzoni.

Unit Testing in Xcode

Il navigatore Test fornisce il modo più semplice per lavorare con i test; lo userai per creare obiettivi di test ed eseguire test sulla tua app.

Creazione di un target di test unitario

Aprire il progetto BullsEye e premere Command-6 per aprire il navigatore Test.

Fare clic sul pulsante + nell’angolo in basso a sinistra, quindi selezionare New Unit Test Target… dal menu:

Accettare il nome predefinito, BullsEyeTests. Quando il bundle del test appare nel navigatore Test, clicca per aprire il bundle nell’editor. Se il bundle non appare automaticamente, risolvi il problema cliccando su uno degli altri navigatori, poi torna al navigatore Test.

Il template predefinito importa il framework di test, XCTest, e definisce una sottoclasse BullsEyeTests di XCTestCase, con setUp(), tearDown(), e metodi di test di esempio.

Ci sono tre modi per eseguire i test:

  1. Product ▸ Test o Command-U. Entrambi eseguono tutte le classi di test.
  2. Clicca il pulsante freccia nel navigatore Test.
  3. Clicca il pulsante diamante nella grondaia.

Puoi anche eseguire un singolo metodo di test cliccando sul suo diamante, sia nel navigatore dei test che nella grondaia.

Prova i diversi modi di eseguire i test per farti un’idea di quanto tempo ci vuole e di come appare. I test di esempio non fanno ancora nulla, quindi sono molto veloci!

Quando tutti i test hanno successo, i diamanti diventano verdi e mostrano i segni di spunta. Puoi cliccare sul diamante grigio alla fine di testPerformanceExample() per aprire il Performance Result:

Non hai bisogno di testPerformanceExample() o testExample() per questo tutorial, quindi cancellali.

Using XCTAssert to Test Models

Prima di tutto, userai le funzioni XCTAssert per testare una funzione centrale del modello di BullsEye: Un oggetto BullsEyeGame calcola correttamente il punteggio di un round?

In BullsEyeTests.swift, aggiungete questa linea appena sotto la dichiarazione import:

@testable import BullsEye

Questo permette ai test unitari di accedere ai tipi e alle funzioni interne di BullsEye.

In cima alla classe BullsEyeTests, aggiungere questa proprietà:

var sut: BullsEyeGame!

Questo crea un segnaposto per un BullsEyeGame, che è il System Under Test (SUT), o l’oggetto che questa classe di test case deve testare.

Poi, sostituite il contenuto di setup() con questo:

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

Questo crea un oggetto BullsEyeGame a livello di classe, così tutti i test in questa classe di test possono accedere alle proprietà e ai metodi dell’oggetto SUT.

Qui, chiamate anche il startNewGame() del gioco, che inizializza il targetValue. Molti dei test useranno targetValue per verificare che il gioco calcoli correttamente il punteggio.

Prima di dimenticare, rilasciate il vostro oggetto SUT in tearDown(). Sostituite il suo contenuto con:

sut = nilsuper.tearDown()
Nota: è buona pratica creare la SUT in setUp() e rilasciarla in tearDown() per assicurarsi che ogni test inizi con una tabula rasa. Per ulteriori discussioni, controlla il post di Jon Reid sull’argomento.

Scrivere il tuo primo test

Ora sei pronto per scrivere il tuo primo test!

Aggiungi il seguente codice alla fine di 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")}

Il nome di un metodo di test inizia sempre con test, seguito da una descrizione di ciò che testa.

È buona pratica formattare il test in given, when e poi sezioni:

  1. Given: Qui, si impostano tutti i valori necessari. In questo esempio, si crea un valore guess in modo da poter specificare quanto differisce da targetValue.
  2. Quando: In questa sezione, eseguirai il codice da testare: Call check(guess:).
  3. Then: Questa è la sezione dove asserite il risultato che vi aspettate con un messaggio che viene stampato se il test fallisce. In questo caso, sut.scoreRound dovrebbe essere uguale a 95 (100 – 5).

Esegui il test cliccando sull’icona a forma di diamante nella grondaia o nel navigatore Test. Questo costruirà ed eseguirà l’applicazione, e l’icona del diamante cambierà in un segno di spunta verde!

Nota: Per vedere una lista completa di XCTestAssertions, vai su Apple’s Assertions Listed by Category.

Debugging a Test

C’è un bug costruito in BullsEyeGame di proposito, e ora ti eserciterai a trovarlo. Per vedere il bug in azione, creerai un test che sottrae 5 da targetValue nella sezione data, e lascia tutto il resto uguale.

Aggiungi il seguente 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")}

La differenza tra guess e targetValue è ancora 5, quindi il punteggio dovrebbe essere ancora 95.

Nel navigatore dei Breakpoint, aggiungi un Breakpoint di fallimento del test. Questo fermerà l’esecuzione del test quando un metodo di test pubblica un’asserzione di fallimento.

Esegui il tuo test, e dovrebbe fermarsi alla linea XCTAssertEqual con un fallimento del test.

Ispeziona sut e guess nella console di debug:

guess è targetValue - 5 ma scoreRound è 105, non 95!

Per indagare ulteriormente, usa il normale processo di debug: Impostare un punto di interruzione all’istruzione when e anche uno in BullsEyeGame.swift, dentro check(guess:), dove crea difference. Poi eseguite di nuovo il test, e passate sopra l’istruzione let difference per controllare il valore di difference nell’applicazione:

Il problema è che difference è negativo, quindi il punteggio è 100 – (-5). Per risolvere questo problema, dovresti usare il valore assoluto di difference. In check(guess:), scomponi la linea corretta e cancella quella errata.

Rimuovi i due breakpoint, ed esegui nuovamente il test per confermare che ora ha successo.

Usando XCTestExpectation per testare operazioni asincrone

Ora che hai imparato come testare i modelli e debuggare i fallimenti dei test, è ora di passare al test del codice asincrono.

Apri il progetto HalfTunes. Usa URLSession per interrogare l’API di iTunes e scaricare campioni di canzoni. Supponiamo che vogliate modificarlo per usare AlamoFire per le operazioni di rete. Per vedere se qualcosa si rompe, dovreste scrivere dei test per le operazioni di rete ed eseguirli prima e dopo aver cambiato il codice.

I metodi

URLSession sono asincroni: ritornano subito, ma non finiscono di funzionare fino a dopo. Per testare i metodi asincroni, si usa XCTestExpectation per far attendere al test il completamento dell’operazione asincrona.

I test asincroni sono di solito lenti, quindi si dovrebbero tenere separati dai test unitari più veloci.

Crea un nuovo obiettivo di test unitario chiamato HalfTunesSlowTests. Apri la classe HalfTunesSlowTests e importa il modulo dell’app HalfTunes appena sotto la dichiarazione import esistente:

@testable import HalfTunes

Tutti i test in questa classe usano il URLSession predefinito per inviare richieste ai server Apple, quindi dichiara un oggetto sut, crealo in setUp() e rilascialo in tearDown().

Sostituite il contenuto della classe HalfTunesSlowTests con:

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

Poi, aggiungete questo test asincrono:

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

Questo test controlla che l’invio di una query valida a iTunes restituisca un codice di stato 200. La maggior parte del codice è uguale a quello che scrivereste nell’app, con queste linee aggiuntive:

  1. expectation(description:): Restituisce un oggetto XCTestExpectation, memorizzato in promise. Il parametro description descrive cosa ci si aspetta che accada.
  2. promise.fulfill(): Chiama questo nella chiusura della condizione di successo del gestore di completamento del metodo asincrono per segnalare che l’aspettativa è stata soddisfatta.
  3. wait(for:timeout:): Mantiene il test in esecuzione fino a quando tutte le aspettative sono soddisfatte, o l’intervallo timeout termina, a seconda di quello che accade prima.

Esegui il test. Se sei connesso a internet, il test dovrebbe impiegare circa un secondo per avere successo dopo che l’app viene caricata nel simulatore.

Failing Fast

Il fallimento fa male, ma non deve durare per sempre.

Per sperimentare il fallimento, basta cancellare la ‘s’ da “itunes” nell’URL:

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

Esegui il test. Fallisce, ma impiega l’intero intervallo di timeout! Questo perché hai dato per scontato che la richiesta avrebbe sempre avuto successo, ed è lì che hai chiamato promise.fulfill(). Poiché la richiesta è fallita, è terminata solo quando il timeout è scaduto.

Puoi migliorare questo e far fallire il test più velocemente cambiando il presupposto: Invece di aspettare che la richiesta vada a buon fine, aspettate solo che venga invocato il gestore di completamento del metodo asincrono. Questo avviene non appena l’applicazione riceve una risposta – OK o errore – dal server, che soddisfa l’aspettativa. Il vostro test può quindi controllare se la richiesta è riuscita.

Per vedere come funziona, create un nuovo test.

Ma prima, correggete il test precedente annullando la modifica che avete fatto al url.
Poi, aggiungete il seguente test alla vostra classe:

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

La differenza chiave è che il semplice inserimento del gestore di completamento soddisfa l’aspettativa, e questo richiede solo circa un secondo per avvenire. Se la richiesta fallisce, le thenasserzioni falliscono.

Esegui il test. Ora dovrebbe impiegare circa un secondo per fallire. Fallisce perché la richiesta è fallita, non perché l’esecuzione del test ha superato il timeout.

Fissate il url, e poi eseguite nuovamente il test per confermare che ora ha successo.

Fake Objects and Interactions

I test asincroni vi danno fiducia che il vostro codice generi un input corretto per un’API asincrona. Potreste anche voler testare che il vostro codice funzioni correttamente quando riceve input da un URLSession, o che aggiorni correttamente il database delle impostazioni predefinite dell’utente o un contenitore iCloud.

La maggior parte delle app interagisce con oggetti di sistema o di libreria – oggetti che non controllate – e i test che interagiscono con questi oggetti possono essere lenti e irripetibili, violando due dei principi FIRST. Invece, potete falsificare le interazioni ricevendo input da stub o aggiornando oggetti mock.

Utilizzate la falsificazione quando il vostro codice ha una dipendenza da un oggetto di sistema o di libreria. Potete farlo creando un oggetto fasullo che interpreti quella parte e iniettando questo fasullo nel vostro codice. Dependency Injection di Jon Reid descrive diversi modi per farlo.

Fake Input From Stub

In questo test, controllerete che updateSearchResults(_:) dell’app analizzi correttamente i dati scaricati dalla sessione, controllando che searchResults.count sia corretto. Il SUT è il controller della vista, e fingerai la sessione con degli stub e alcuni dati pre-caricati.

Vai nel navigatore dei test e aggiungi un nuovo obiettivo di test unitario. Chiamalo HalfTunesFakeTests. Aprite HalfTunesFakeTests.swift e importa il modulo dell’app HalfTunes appena sotto la dichiarazione import:

@testable import HalfTunes

Ora, sostituisci il contenuto della classe HalfTunesFakeTests con questo:

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

Questo dichiara il SUT, che è un SearchViewController, lo crea in setUp() e lo rilascia in tearDown():

Nota: la SUT è il view controller, perché HalfTunes ha un enorme problema di view controller – tutto il lavoro è fatto in SearchViewController.veloce. Spostare il codice di rete in un modulo separato ridurrebbe questo problema e, inoltre, renderebbe i test più facili.

In seguito, avrete bisogno di alcuni dati JSON di esempio che la vostra finta sessione fornirà al vostro test. Bastano pochi elementi, quindi per limitare i risultati del download in iTunes aggiungi &limit=3 alla stringa dell’URL:

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

Copia questo URL e incollalo in un browser. Questo scarica un file chiamato 1.txt, 1.txt.js o simile. Fai un’anteprima per confermare che è un file JSON, poi rinominalo abbaData.json.

Ora, torna in Xcode e vai al navigatore del progetto. Aggiungete il file al gruppo HalfTunesFakeTests.

Il progetto HalfTunes contiene il file di supporto DHURLSessionMock.swift. Questo definisce un semplice protocollo chiamato DHURLSession, con metodi (stub) per creare un task di dati con un URL o un URLRequest. Definisce anche URLSessionMock, che è conforme a questo protocollo con inizializzatori che vi permettono di creare un oggetto mock URLSession con la vostra scelta di dati, risposta ed errore.

Per impostare il fake, andate su HalfTunesFakeTests.swift e aggiungete quanto segue in setUp(), dopo la dichiarazione che crea la 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

Questo imposta i dati e la risposta fasulli e crea il falso oggetto sessione. Infine, alla fine, inietta la finta sessione nell’app come proprietà di sut.

Ora, sei pronto a scrivere il test che controlla se chiamando updateSearchResults(_:) si analizzano i dati falsi. Aggiungete il seguente 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")}

Dovete ancora scriverlo come un test asincrono perché lo stub sta fingendo di essere un metodo asincrono.

L’asserzione when è che searchResults sia vuoto prima che il task dei dati venga eseguito. Questo dovrebbe essere vero, perché avete creato una SUT completamente nuova in setUp().

I dati falsi contengono il JSON per tre oggetti Track, quindi l’asserzione then è che l’array searchResults del view controller contiene tre elementi.

Eseguite il test. Dovrebbe riuscire abbastanza velocemente perché non c’è nessuna connessione di rete reale!

Fake Update to Mock Object

Il test precedente ha usato uno stub per fornire input da un oggetto falso. Ora userai un oggetto finto per verificare che il tuo codice aggiorni correttamente UserDefaults.

Riapri il progetto BullsEye. L’applicazione ha due stili di gioco: L’utente muove il cursore per far corrispondere il valore target o indovina il valore target dalla posizione del cursore. Un controllo segmentato nell’angolo in basso a destra cambia lo stile di gioco e lo salva nelle impostazioni predefinite dell’utente.

Il prossimo test controllerà che l’applicazione salvi correttamente la proprietà gameStyle.

Nel navigatore dei test, cliccate su New Unit Test Class e nominatelo BullsEyeMockTests. Aggiungi il seguente sotto la dichiarazione import:

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

MockUserDefaults sovrascrive set(_:forKey:) per incrementare il flag gameStyleChanged. Spesso, vedrete test simili che impostano una variabile Bool, ma incrementare un Int vi dà più flessibilità – per esempio, il vostro test potrebbe controllare che il metodo sia chiamato solo una volta.

Dichiarate la SUT e l’oggetto mock in BullsEyeMockTests:

var sut: ViewController!var mockUserDefaults: MockUserDefaults!

Poi, sostituite i predefiniti setUp() e tearDown() con questo:

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

Questo crea la SUT e l’oggetto mock e inietta l’oggetto mock come proprietà della SUT.

Ora, sostituite i due metodi di test predefiniti nel template con questo:

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

L’asserzione when è che il flag gameStyleChanged sia 0 prima che il metodo di test cambi il controllo segmentato. Quindi, se anche l’asserzione then è vera, significa che set(_:forKey:) è stato chiamato esattamente una volta.

Esegui il test; dovrebbe avere successo.

UI Testing in Xcode

UI testing ti permette di testare le interazioni con l’interfaccia utente. Il test UI funziona trovando gli oggetti UI di un’app con delle query, sintetizzando gli eventi e poi inviando gli eventi a quegli oggetti. L’API permette di esaminare le proprietà e lo stato di un oggetto UI per confrontarli con lo stato previsto.

Nel navigatore dei test del progetto BullsEye, aggiungere un nuovo target di test UI. Controllate che il Target da testare sia BullsEye, e accettate il nome predefinito BullsEyeUITests.

Aprite BullsEyeUITests.swift e aggiungete questa proprietà all’inizio della classe BullsEyeUITests:

var app: XCUIApplication!

In setUp(), sostituite la dichiarazione XCUIApplication().launch() con la seguente:

app = XCUIApplication()app.launch()

Cambiate il nome di testExample() con testGameStyleSwitch().

Apri una nuova riga in testGameStyleSwitch() e fai clic sul pulsante rosso Record in fondo alla finestra dell’editor:

Questo apre l’app nel simulatore in una modalità che registra le tue interazioni come comandi di prova. Una volta caricata l’app, tocca il segmento Slide dell’interruttore di stile del gioco e l’etichetta superiore. Poi, fai clic sul pulsante Xcode Record per fermare la registrazione.

Ora hai le seguenti tre linee in testGameStyleSwitch():

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

Il Recorder ha creato del codice per testare le stesse azioni che hai testato nell’app. Invia un tap allo slider e all’etichetta. Le userai come base per creare il tuo test UI.
Se vedi altre dichiarazioni, cancellale.

La prima linea duplica la proprietà che hai creato in setUp(), quindi cancella quella linea. Non hai ancora bisogno di toccare nulla, quindi cancella anche .tap() alla fine delle linee 2 e 3. Ora, apri il piccolo menu accanto a e seleziona segmentedControls.buttons.

Quello che ti rimane dovrebbe essere il seguente:

app.segmentedControls.buttonsapp.staticTexts

Tocca qualsiasi altro oggetto per lasciare che il registratore ti aiuti a trovare il codice a cui puoi accedere nei tuoi test. Ora, sostituisci quelle linee con questo codice per creare una data sezione:

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

Ora che hai i nomi per i due pulsanti nel controllo segmentato, e le due possibili etichette superiori, aggiungi il seguente codice qui sotto:

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

Questo controlla per vedere se l’etichetta corretta esiste quando tap() su ogni pulsante nel controllo segmentato. Esegui il test – tutte le asserzioni dovrebbero avere successo.

Performance Testing

Dalla documentazione di Apple: Un test delle prestazioni prende un blocco di codice che si vuole valutare e lo esegue dieci volte, raccogliendo il tempo medio di esecuzione e la deviazione standard per le esecuzioni. La media di queste singole misurazioni forma un valore per l’esecuzione del test che può poi essere confrontato con una linea di base per valutare il successo o il fallimento.

È molto semplice scrivere un test delle prestazioni: Basta mettere il codice che vuoi misurare nella chiusura del measure().

Per vederlo in azione, riapri il progetto HalfTunes e, in HalfTunesFakeTests.swift, aggiungi il seguente 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) }}

Esegui il test, poi clicca sull’icona che appare accanto all’inizio della chiusura measure() per vedere le statistiche.

Clicca Set Baseline per impostare un tempo di riferimento. Poi, esegui di nuovo il test delle prestazioni e visualizza il risultato – potrebbe essere migliore o peggiore della linea di base. Il pulsante Modifica ti permette di reimpostare la linea di base su questo nuovo risultato.

Le linee di base sono memorizzate per configurazione del dispositivo, quindi puoi avere lo stesso test in esecuzione su diversi dispositivi, e fare in modo che ognuno mantenga una linea di base diversa a seconda della velocità del processore della configurazione specifica, della memoria, ecc.

Ogni volta che apporti modifiche a un’applicazione che potrebbero avere un impatto sulle prestazioni del metodo in prova, esegui nuovamente il test delle prestazioni per vedere come si confronta con la linea di base.

Code Coverage

Lo strumento di copertura del codice vi dice quale codice dell’app viene effettivamente eseguito dai vostri test, così sapete quali parti del codice dell’app non sono (ancora) testate.

Per abilitare la copertura del codice, modifica l’azione Test dello schema e seleziona la casella di controllo Gather coverage for sotto la scheda Options:

Esegui tutti i test (Command-U), poi apri il navigatore Report (Command-9). Selezionate Coverage sotto la voce in alto nella lista:

Fate clic sul triangolo di rivelazione per vedere la lista di funzioni e chiusure in SearchViewController.swift:

Scorrete fino a updateSearchResults(_:) per vedere che la copertura è dell’87,9%.

Fate clic sul pulsante freccia per questa funzione per aprire il file sorgente della funzione. Passando il mouse sulle annotazioni di copertura nella barra laterale destra, le sezioni di codice si evidenziano in verde o in rosso:

Le annotazioni di copertura mostrano quante volte un test colpisce ogni sezione di codice; le sezioni che non sono state chiamate sono evidenziate in rosso. Come ci si aspetterebbe, il ciclo for è stato eseguito 3 volte, ma niente nei percorsi degli errori è stato eseguito.

Per aumentare la copertura di questa funzione, si potrebbe duplicare abbaData.json, poi modificarlo in modo che causi i diversi errori. Per esempio, cambiate "results" in "result" per un test che colpisce print("Results key not found in dictionary").

100% di copertura?

Quanto dovreste sforzarvi di ottenere il 100% di copertura del codice? Cerca su Google “100% unit test coverage” e troverai una serie di argomenti a favore e contro questo, insieme al dibattito sulla definizione stessa di “100% coverage”. Gli argomenti contro dicono che l’ultimo 10-15% non vale lo sforzo. Gli argomenti a favore dicono che l’ultimo 10-15% è il più importante, perché è così difficile da testare. Cerca su Google “hard to unit test bad design” per trovare argomenti persuasivi che il codice non testabile è un segno di problemi di progettazione più profondi.

Dove andare da qui?

Ora avete alcuni grandi strumenti da usare per scrivere test per i vostri progetti. Spero che questo tutorial sui test unitari e sull’interfaccia utente di iOS ti abbia dato la fiducia necessaria per testare tutto!

Puoi scaricare la versione completata del progetto usando il pulsante Download Materials in cima o in fondo a questo tutorial. Continua a sviluppare le tue abilità aggiungendo altri test tuoi.

Ecco alcune risorse per ulteriori studi:

  • Ci sono diversi video del WWDC sull’argomento dei test. Due buoni dal WWDC17 sono: Engineering for Testability e Testing Tips & Tricks.
  • Il prossimo passo è l’automazione: Integrazione continua e consegna continua. Iniziate da Automating the Test Process with Xcode Server e xcodebuild di Apple, e dall’articolo di Wikipedia sulla consegna continua, che si basa sull’esperienza di ThoughtWorks.
  • Se avete già un’app ma non avete ancora scritto i test per essa, potreste voler fare riferimento a Working Effectively with Legacy Code di Michael Feathers, perché il codice senza test è codice legacy!
  • Gli archivi delle app campione di Quality Coding di Jon Reid sono ottimi per imparare di più sul Test Driven Development.

raywenderlich.com Weekly

La newsletter di raywenderlich.com è il modo più semplice per rimanere aggiornato su tutto ciò che devi sapere come sviluppatore mobile.

Ottieni un digest settimanale dei nostri tutorial e corsi, e ricevi un corso di approfondimento via email come bonus!

Valutazione media

4.7/5

Aggiungi una valutazione per questo contenuto

Accedi per aggiungere una valutazione

90 valutazioni

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.