iOS Testy jednostkowe i testowanie UI Samouczek

Uwaga o aktualizacji: Michael Katz zaktualizował ten samouczek dla Xcode 10.1, Swift 4.2 i iOS 12. Audrey Tam napisała oryginał.

Pisanie testów nie jest efektowne, ale ponieważ testy powstrzymują twoją błyszczącą aplikację przed przekształceniem się w pełen błędów kawałek złomu, jest to konieczne. Jeśli czytasz ten poradnik, wiesz już, że powinieneś pisać testy dla swojego kodu i UI, ale możesz nie wiedzieć jak.

Możesz mieć działającą aplikację, ale chcesz przetestować zmiany, które wprowadzasz, aby ją rozszerzyć. Być może masz już napisane testy, ale nie jesteś pewien, czy są to właściwe testy. Albo rozpocząłeś pracę nad nową aplikacją i chcesz testować na bieżąco.

Ten tutorial pokaże Ci:

  • Jak używać nawigatora Test w Xcode do testowania modelu aplikacji i metod asynchronicznych
  • Jak udawać interakcje z obiektami biblioteki lub systemu za pomocą stubów i mocks
  • Jak testować UI i wydajność
  • Jak używać narzędzia pokrycia kodu

Po drodze, poznasz część słownictwa używanego przez testujących ninja.

Dowiedzieć się co testować

Przed napisaniem jakichkolwiek testów, ważne jest by znać podstawy. Co potrzebujesz testować?

Jeśli twoim celem jest rozszerzenie istniejącej aplikacji, powinieneś najpierw napisać testy dla każdego komponentu, który planujesz zmienić.

Ogólnie, testy powinny obejmować:

  • Rdzenną funkcjonalność: Klasy i metody modelu oraz ich interakcje z kontrolerem
  • Najczęstsze przepływy pracy UI
  • Warunki brzegowe
  • Poprawki błędów

Najlepsze praktyki testowania

Akronim FIRST opisuje zwięzły zestaw kryteriów dla skutecznych testów jednostkowych. Kryteria te są następujące:

  • Szybkie: Testy powinny działać szybko.
  • Independent/Isolated: Testy nie powinny dzielić stanu między sobą.
  • Powtarzalność: Powinieneś uzyskać te same wyniki za każdym razem, gdy uruchamiasz test. Zewnętrzni dostawcy danych lub problemy ze współbieżnością mogą powodować sporadyczne niepowodzenia.
  • Samoweryfikacja: Testy powinny być w pełni zautomatyzowane. Wyjście powinno być albo „pass” albo „fail”, a nie polegać na interpretacji pliku logu przez programistę.
  • Terminowość: Idealnie, testy powinny być pisane przed napisaniem kodu produkcyjnego, który testują (Test-Driven Development).

Postępowanie zgodnie z zasadami FIRST sprawi, że twoje testy będą przejrzyste i pomocne, zamiast zamieniać się w blokady drogowe dla twojej aplikacji.

Rozpoczęcie

Zacznij od pobrania materiałów do projektu za pomocą przycisku Pobierz materiały na górze lub na dole tego poradnika. Istnieją dwa oddzielne projekty startowe: BullsEye i HalfTunes.

  • BullsEye jest oparty na przykładowej aplikacji w iOS Apprentice. Logika gry znajduje się w klasie BullsEyeGame, którą przetestujesz podczas tego samouczka.
  • HalfTunes jest uaktualnioną wersją przykładowej aplikacji z URLSession Tutorial. Użytkownicy mogą odpytywać API iTunes w poszukiwaniu piosenek, a następnie pobierać i odtwarzać fragmenty utworów.

Testy jednostkowe w Xcode

Nawigator Test zapewnia najprostszy sposób pracy z testami; użyjemy go do tworzenia celów testowych i uruchamiania testów przeciwko naszej aplikacji.

Tworzenie celu testu jednostkowego

Otwórz projekt BullsEye i naciśnij Command-6, aby otworzyć nawigator Testy.

Kliknij przycisk + w lewym dolnym rogu, a następnie wybierz Nowy cel testu jednostkowego… z menu:

Zaakceptuj domyślną nazwę, BullsEyeTests. Gdy wiązka testów pojawi się w Nawigatorze testów, kliknij, aby otworzyć ją w edytorze. Jeśli pakiet nie pojawi się automatycznie, rozwiąż problem, klikając jeden z innych nawigatorów, a następnie wróć do nawigatora Test.

Domyślny szablon importuje szkielet testowy, XCTest, i definiuje BullsEyeTests podklasę XCTestCase, z setUp(), tearDown() i przykładowymi metodami testowymi.

Istnieją trzy sposoby uruchamiania testów:

  1. Product ▸ Test lub Command-U. Oba uruchamiają wszystkie klasy testowe. Oba te sposoby uruchamiają wszystkie klasy testowe.
  2. Kliknij przycisk strzałki w nawigatorze Test.
  3. Kliknij przycisk rombu w rynnie.

Możesz również uruchomić pojedynczą metodę testową, klikając jej diament, albo w Nawigatorze testów, albo w rynnie.

Wypróbuj różne sposoby uruchamiania testów, aby zorientować się, jak długo to trwa i jak wygląda. Przykładowe testy jeszcze nic nie robią, więc działają naprawdę szybko!

Gdy wszystkie testy się powiodą, diamenty zmienią kolor na zielony i pokażą znaczniki wyboru. Możesz kliknąć szary diament na końcu testPerformanceExample(), aby otworzyć Wynik wydajności:

Nie potrzebujesz testPerformanceExample() ani testExample() do tego samouczka, więc je usuń.

Używanie XCTAssert do testowania modeli

Po pierwsze, użyjesz funkcji XCTAssert do przetestowania podstawowej funkcji modelu BullsEye: Czy obiekt BullsEyeGame poprawnie oblicza wynik rundy?

W BullsEyeTests.swift, dodaj tę linię tuż pod importoświadczeniem:

@testable import BullsEye

To daje testom jednostkowym dostęp do wewnętrznych typów i funkcji w BullsEye.

Na szczycie klasy BullsEyeTests dodaj tę właściwość:

var sut: BullsEyeGame!

Tworzy to uchwyt dla BullsEyeGame, który jest System Under Test (SUT) lub obiektem, który ta klasa testowa chce przetestować.

Następnie, zastąp zawartość setup() tym:

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

Tworzy to obiekt BullsEyeGame na poziomie klasy, więc wszystkie testy w tej klasie testowej mogą mieć dostęp do właściwości i metod obiektu SUT.

Tutaj również wywołujesz startNewGame() gry, który inicjalizuje targetValue. Wiele testów będzie używać targetValue do sprawdzenia, czy gra poprawnie oblicza wynik.

Zanim zapomnisz, zwolnij swój obiekt SUT w tearDown(). Zamień jego zawartość na:

sut = nilsuper.tearDown()
Uwaga: Dobrą praktyką jest tworzenie SUT w setUp() i zwalnianie go w tearDown(), aby mieć pewność, że każdy test zaczyna się od czystego konta. Aby uzyskać więcej informacji na ten temat, sprawdź post Jona Reida na ten temat.

Pisanie swojego pierwszego testu

Teraz, jesteś gotowy do napisania swojego pierwszego testu!

Dodaj następujący kod na końcu 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")}

Nazwa metody testowej zawsze zaczyna się od test, a po niej następuje opis tego, co testuje.

Dobrą praktyką jest formatowanie testu na sekcje given, when i then:

  1. Given: Tutaj ustawiasz wszelkie potrzebne wartości. W tym przykładzie, tworzysz wartość guess, abyś mógł określić jak bardzo różni się ona od targetValue.
  2. Kiedy: W tej części wykonasz testowany kod: Call check(guess:).
  3. Then: To jest sekcja, w której zapewnisz wynik, którego oczekujesz, wraz z komunikatem, który wydrukuje się, jeśli test się nie powiedzie. W tym przypadku sut.scoreRound powinno być równe 95 (100 – 5).

Uruchom test, klikając ikonę diamentu w rynnie lub w nawigatorze Test. Spowoduje to zbudowanie i uruchomienie aplikacji, a ikona diamentu zmieni się w zielony znak zaznaczenia!

Uwaga: Aby zobaczyć pełną listę XCTestAssertions, przejdź do Apple’s Assertions Listed by Category.

Debugging a Test

Celowo wbudowano błąd w BullsEyeGame, a teraz przećwiczysz jego znajdowanie. Aby zobaczyć ten błąd w akcji, stworzysz test, który odejmuje 5 od targetValue w podanej sekcji, a wszystko inne pozostawia bez zmian.

Dodaj następujący 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")}

Różnica między guess a targetValue nadal wynosi 5, więc wynik nadal powinien wynosić 95.

W nawigatorze Breakpoint dodaj punkt przerwania Test Failure Breakpoint. Spowoduje to zatrzymanie przebiegu testu, gdy metoda testowa zgłosi asercję o niepowodzeniu.

Rozpocznij test, a powinien on zatrzymać się w linii XCTAssertEqual z niepowodzeniem testu.

Sprawdź sut i guess w konsoli debugowania:

guess jest targetValue - 5, ale scoreRound jest 105, a nie 95!

Aby zbadać dalej, użyj normalnego procesu debugowania: Ustaw punkt przerwania w instrukcji when, a także jeden w BullsEyeGame.swift, wewnątrz check(guess:), gdzie tworzy difference. Następnie uruchom test ponownie i przejdź przez instrukcję let difference, aby sprawdzić wartość difference w aplikacji:

Problem polega na tym, że difference jest ujemne, więc wynik jest 100 – (-5). Aby to naprawić, powinieneś użyć wartości bezwzględnej difference. W check(guess:) odkomentuj poprawny wiersz i usuń niepoprawny.

Usuń dwa punkty przerwania i uruchom test ponownie, aby potwierdzić, że teraz się powiódł.

Używanie XCTestExpectation do testowania operacji asynchronicznych

Teraz, gdy już nauczyłeś się testować modele i usuwać błędy w testach, czas przejść do testowania kodu asynchronicznego.

Otwórz projekt HalfTunes. Używa on URLSession do odpytywania API iTunes i pobierania próbek piosenek. Załóżmy, że chcesz go zmodyfikować, aby używał AlamoFire do operacji sieciowych. Aby sprawdzić, czy coś się popsuło, powinieneś napisać testy dla operacji sieciowych i uruchomić je przed i po zmianie kodu.

URLSession Metody są asynchroniczne: zwracają od razu, ale nie kończą działania aż do później. Aby testować metody asynchroniczne, używasz XCTestExpectation, aby twój test czekał na zakończenie operacji asynchronicznej.

Testy asynchroniczne są zwykle powolne, więc powinieneś trzymać je oddzielnie od szybszych testów jednostkowych.

Utwórz nowy cel testu jednostkowego o nazwie HalfTunesSlowTests. Otwórz klasę HalfTunesSlowTests i zaimportuj moduł aplikacji HalfTunes tuż poniżej istniejącej deklaracji import:

@testable import HalfTunes

Wszystkie testy w tej klasie używają domyślnego URLSession do wysyłania żądań do serwerów Apple, więc zadeklaruj obiekt sut, utwórz go w setUp() i zwolnij w tearDown().

Zamień zawartość klasy HalfTunesSlowTests na:

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

Następnie dodaj ten test asynchroniczny:

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

Test ten sprawdza, czy wysłanie prawidłowego zapytania do iTunes zwraca kod stanu 200. Większość kodu jest taka sama jak w aplikacji, z tymi dodatkowymi liniami:

  1. expectation(description:): Zwraca obiekt XCTestExpectation, przechowywany w promise. Parametr description opisuje, czego oczekujesz, aby się wydarzyło.
  2. promise.fulfill(): Wywołaj to w zamknięciu warunku sukcesu obsługi zakończenia metody asynchronicznej, aby oflagować, że oczekiwanie zostało spełnione.
  3. wait(for:timeout:): Utrzymuje test w ruchu, dopóki wszystkie oczekiwania nie zostaną spełnione lub nie skończy się timeout interwał, w zależności od tego, co nastąpi wcześniej.

Uruchom test. Jeśli jesteś podłączony do Internetu, test powinien zająć około sekundy, aby odnieść sukces po załadowaniu aplikacji w symulatorze.

Szybka porażka

Porażka boli, ale nie musi trwać wiecznie.

Aby doświadczyć porażki, po prostu usuń „s” z „itunes” w adresie URL:

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

Rozpocznij test. Nie powiedzie się, ale zajmie to pełny przedział czasu! Dzieje się tak, ponieważ zakładałeś, że żądanie zawsze się powiedzie, i to właśnie tam zadzwoniłeś do promise.fulfill(). Ponieważ żądanie nie powiodło się, zakończyło się dopiero wtedy, gdy upłynął limit czasu.

Możesz to poprawić i sprawić, by test zakończył się szybciej, zmieniając założenie: Zamiast czekać na powodzenie żądania, czekaj tylko do momentu wywołania handler’a zakończenia metody asynchronicznej. Dzieje się to w momencie, gdy aplikacja otrzyma odpowiedź – OK lub błąd – z serwera, która spełnia oczekiwania. Twój test może wtedy sprawdzić, czy żądanie zakończyło się sukcesem.

Aby zobaczyć, jak to działa, utwórz nowy test.

Najpierw jednak napraw poprzedni test, cofając zmianę, którą wprowadziłeś do url.
Następnie dodaj następujący test do swojej klasy:

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

Kluczową różnicą jest to, że samo wejście do handler’a uzupełniania spełnia oczekiwania, a to trwa tylko około sekundy. Jeśli żądanie się nie powiedzie, asercje then zawiodą.

Rozpocznij test. Powinien on teraz zająć około sekundy, aby zakończyć się niepowodzeniem. Zawodzi, ponieważ żądanie się nie powiodło, a nie dlatego, że przebieg testu przekroczył timeout.

Napraw url, a następnie uruchom test ponownie, aby potwierdzić, że teraz się powiedzie.

Faking Objects and Interactions

Testy asynchroniczne dają ci pewność, że twój kod generuje poprawne dane wejściowe do asynchronicznego API. Możesz również chcieć przetestować, czy Twój kod działa poprawnie, gdy otrzymuje dane wejściowe od URLSession, lub czy poprawnie aktualizuje bazę danych ustawień użytkownika lub kontener iCloud.

Większość aplikacji wchodzi w interakcje z obiektami systemowymi lub bibliotecznymi – obiektami, których nie kontrolujesz – a testy, które wchodzą w interakcje z tymi obiektami mogą być powolne i niepowtarzalne, naruszając dwie z zasad FIRST. Zamiast tego, możesz sfałszować interakcje, otrzymując dane wejściowe ze stubów lub aktualizując mock objects.

Wykorzystaj fałszerstwo, gdy twój kod ma zależność od obiektu systemowego lub bibliotecznego. Możesz to zrobić tworząc fałszywy obiekt, który będzie odgrywał tę rolę i wstrzykując go do swojego kodu. Dependency Injection autorstwa Jona Reida opisuje kilka sposobów, jak to zrobić.

Fake Input From Stub

W tym teście sprawdzisz, czy updateSearchResults(_:) aplikacji poprawnie przetwarza dane pobrane przez sesję, poprzez sprawdzenie, czy searchResults.count jest poprawne. SUT jest kontrolerem widoku, a ty będziesz udawał sesję za pomocą stubów i kilku wstępnie pobranych danych.

Przejdź do nawigatora Test i dodaj nowy cel testu jednostkowego. Nazwij go HalfTunesFakeTests. Otwórz HalfTunesFakeTests.swift i zaimportuj moduł HalfTunes app tuż poniżej deklaracji import:

@testable import HalfTunes

Teraz zastąp zawartość klasy HalfTunesFakeTests tym:

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

To deklaruje SUT, który jest SearchViewController, tworzy go w setUp() i zwalnia w tearDown():

Uwaga: SUT jest kontrolerem widoku, ponieważ HalfTunes ma ogromny problem z kontrolerem widoku – cała praca jest wykonywana w SearchViewController.swift. Przeniesienie kodu sieciowego do oddzielnego modułu zmniejszyłoby ten problem, a także ułatwiłoby testowanie.

Następnie będziesz potrzebował przykładowych danych JSON, które twoja fałszywa sesja dostarczy do twojego testu. Wystarczy kilka elementów, więc aby ograniczyć wyniki pobierania w iTunes dołącz &limit=3 do ciągu URL:

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

Skopiuj ten adres URL i wklej go do przeglądarki. Spowoduje to pobranie pliku o nazwie 1.txt, 1.txt.js lub podobnej. Obejrzyj go, aby potwierdzić, że jest to plik JSON, a następnie zmień jego nazwę na abbaData.json.

Teraz wróć do Xcode i przejdź do nawigatora projektu. Dodaj plik do grupy HalfTunesFakeTests.

Projekt HalfTunes zawiera plik pomocniczy DHURLSessionMock.swift. Definiuje on prosty protokół o nazwie DHURLSession, z metodami (stubami) do tworzenia zadania danych z URL lub URLRequest. Definiuje on również URLSessionMock, który jest zgodny z tym protokołem z inicjalizatorami, które pozwalają utworzyć obiekt mock URLSession z wybranymi danymi, odpowiedzią i błędem.

Aby skonfigurować fałszywkę, przejdź do HalfTunesFakeTests.swift i dodaj następujące elementy w setUp(), po oświadczeniu, które tworzy 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

To ustawia fałszywe dane i odpowiedź oraz tworzy fałszywy obiekt sesji. W końcu, na końcu, wstrzykuje fałszywą sesję do aplikacji jako właściwość sut.

Teraz, jesteś gotowy do napisania testu, który sprawdza, czy wywołanie updateSearchResults(_:) przetwarza fałszywe dane. Dodaj następujący 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")}

Ciągle musisz napisać to jako test asynchroniczny, ponieważ stub udaje metodę asynchroniczną.

Twierdzenie when jest takie, że searchResults jest puste przed uruchomieniem zadania danych. To powinno być prawdą, ponieważ stworzyłeś zupełnie nowy SUT w setUp().

Fałszywe dane zawierają JSON dla trzech obiektów Track, więc asercja then jest taka, że tablica searchResults kontrolera widoku zawiera trzy elementy.

Rozpocznij test. Powinien on zakończyć się sukcesem dość szybko, ponieważ nie ma żadnego prawdziwego połączenia sieciowego!

Fałszywe uaktualnienie obiektu pozorowanego

Poprzedni test użył szablonu do dostarczenia danych wejściowych z obiektu pozorowanego. Następnie użyjesz obiektu pozorowanego, aby sprawdzić, czy Twój kod poprawnie aktualizuje UserDefaults.

Otwórz ponownie projekt BullsEye. Aplikacja ma dwa style gry: Użytkownik albo przesuwa suwak, aby dopasować go do wartości docelowej, albo zgaduje wartość docelową na podstawie pozycji suwaka. Segmentowa kontrolka w prawym dolnym rogu przełącza styl gry i zapisuje go w domyślnych ustawieniach użytkownika.

Twoim następnym testem sprawdzisz, czy aplikacja poprawnie zapisuje właściwość gameStyle.

W nawigatorze Testy kliknij New Unit Test Class i nazwij ją BullsEyeMockTests. Dodaj następujące elementy poniżej deklaracji import:

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

MockUserDefaults nadpisuje set(_:forKey:), aby inkrementować flagę gameStyleChanged. Często zobaczysz podobne testy, które ustawiają zmienną Bool, ale inkrementacja Int daje ci większą elastyczność – na przykład, twój test może sprawdzić, czy metoda jest wywoływana tylko raz.

Deklarujemy SUT i obiekt wyśmiewający w BullsEyeMockTests:

var sut: ViewController!var mockUserDefaults: MockUserDefaults!

Następnie zastępujemy domyślne setUp() i tearDown() tym:

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

Tworzy to SUT i obiekt wyśmiewający oraz wstrzykuje obiekt wyśmiewający jako właściwość SUT.

Teraz zastąp dwie domyślne metody testowe w szablonie tym:

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

Twierdzenie when jest takie, że flaga gameStyleChanged ma wartość 0 zanim metoda testowa zmieni kontrolę segmentu. Jeśli więc asercja then jest również prawdziwa, oznacza to, że set(_:forKey:) został wywołany dokładnie raz.

Uruchom test; powinien się powieść.

Testowanie UI w Xcode

Testowanie UI pozwala na testowanie interakcji z interfejsem użytkownika. Testowanie UI działa poprzez znalezienie obiektów UI aplikacji za pomocą zapytań, syntezę zdarzeń, a następnie wysłanie zdarzeń do tych obiektów. API pozwala na zbadanie właściwości i stanu obiektu UI w celu porównania ich z oczekiwanym stanem.

W nawigatorze Test projektu BullsEye, dodaj nowy cel testu UI. Sprawdź, czy celem do przetestowania jest BullsEye, a następnie zaakceptuj domyślną nazwę BullsEyeUITests.

Otwórz BullsEyeUITests.swift i dodaj tę właściwość na górze klasy BullsEyeUITests:

var app: XCUIApplication!

W setUp() zastąp stwierdzenie XCUIApplication().launch() następującym:

app = XCUIApplication()app.launch()

Zmień nazwę testExample() na testGameStyleSwitch().

Otwórz nowy wiersz w testGameStyleSwitch() i kliknij czerwony przycisk Nagraj u dołu okna edytora:

To otwiera aplikację w symulatorze w trybie, który rejestruje interakcje jako polecenia testowe. Po załadowaniu aplikacji, dotknij segmentu Slide przełącznika stylu gry i górnej etykiety. Następnie kliknij przycisk Xcode Record, aby zatrzymać nagrywanie.

Teraz masz następujące trzy linie w testGameStyleSwitch():

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

Rejestrator utworzył kod do testowania tych samych działań, które testowałeś w aplikacji. Wyślij stuknięcie do suwaka i etykiety. Użyjesz ich jako bazy do stworzenia własnego testu UI.
Jeśli widzisz jakieś inne stwierdzenia, po prostu je usuń.

Pierwsza linia duplikuje właściwość, którą utworzyłeś w setUp(), więc usuń tę linię. Nie musisz jeszcze niczego stukać, więc usuń również .tap() na końcu linii 2 i 3. Teraz otwórz małe menu obok i wybierz segmentedControls.buttons.

To, co Ci zostało, powinno wyglądać następująco:

app.segmentedControls.buttonsapp.staticTexts

Tknij inne obiekty, aby rejestrator pomógł Ci znaleźć kod, do którego możesz mieć dostęp w swoich testach. Teraz zastąp te linie tym kodem, aby utworzyć daną sekcję:

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

Teraz, gdy masz nazwy dla dwóch przycisków w kontrolce segmentowej i dwie możliwe górne etykiety, dodaj następujący kod poniżej:

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

Sprawdza on, czy istnieje poprawna etykieta, gdy tap() na każdym przycisku w kontrolce segmentowej. Uruchom test – wszystkie asercje powinny się powieść.

Testy wydajnościowe

Z dokumentacji Apple: Test wydajnościowy bierze blok kodu, który chcesz ocenić, i uruchamia go dziesięć razy, zbierając średni czas wykonania i odchylenie standardowe dla przebiegów. Uśrednienie tych indywidualnych pomiarów tworzy wartość dla przebiegu testu, która może być następnie porównana z wartością bazową, aby ocenić sukces lub porażkę.

To jest bardzo proste, aby napisać test wydajności: Wystarczy umieścić kod, który chcesz zmierzyć, w zamknięciu measure().

Aby zobaczyć to w akcji, otwórz ponownie projekt HalfTunes i w HalfTunesFakeTests.swift, dodaj następujący 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) }}

Uruchom test, a następnie kliknij ikonę, która pojawia się obok początku zamknięcia measure() trailing, aby zobaczyć statystyki.

Kliknij Set Baseline, aby ustawić czas odniesienia. Następnie uruchom ponownie test wydajności i zobacz wynik – może być lepszy lub gorszy od linii bazowej. Przycisk Edytuj pozwala zresetować linię bazową do tego nowego wyniku.

Linie bazowe są przechowywane w konfiguracji urządzenia, więc możesz mieć ten sam test wykonywany na kilku różnych urządzeniach, a każde z nich utrzymuje inną linię bazową zależną od konkretnej konfiguracji prędkości procesora, pamięci itp.

Za każdym razem, gdy wprowadzasz zmiany w aplikacji, które mogą mieć wpływ na wydajność testowanej metody, uruchom ponownie test wydajności, aby zobaczyć, jak wypada w porównaniu z linią bazową.

Pokrycie kodu

Narzędzie pokrycia kodu mówi ci, jaki kod aplikacji jest faktycznie uruchamiany przez twoje testy, więc wiesz, jakie części kodu aplikacji nie są (jeszcze) testowane.

Aby włączyć pokrycie kodu, edytuj akcję Test schematu i zaznacz pole wyboru Gather coverage for w zakładce Options:

Run all tests (Command-U), a następnie otwórz okno Report navigator (Command-9). Wybierz Pokrycie pod górną pozycją na tej liście:

Kliknij trójkąt ujawniania, aby zobaczyć listę funkcji i zamknięć w SearchViewController.swift:

Przewiń w dół do updateSearchResults(_:), aby zobaczyć, że pokrycie wynosi 87.9%.

Kliknij przycisk strzałki dla tej funkcji, aby otworzyć plik źródłowy funkcji. Gdy przesuwasz kursor myszy nad adnotacjami pokrycia w prawym pasku bocznym, sekcje kodu podświetlają się na zielono lub czerwono:

Adnotacje pokrycia pokazują, ile razy test trafia w każdą sekcję kodu; sekcje, które nie zostały wywołane, są podświetlone na czerwono. Jak można się spodziewać, pętla for została uruchomiona 3 razy, ale nic w ścieżkach błędów nie zostało wykonane.

Aby zwiększyć pokrycie tej funkcji, możesz zduplikować plik abbaData.json, a następnie edytować go tak, aby powodował różne błędy. Na przykład, zmień "results" na "result" dla testu, który trafia print("Results key not found in dictionary").

100% Pokrycie?

Jak bardzo powinieneś dążyć do 100% pokrycia kodu? Google „100% pokrycie testami jednostkowymi” i znajdziesz szereg argumentów za i przeciw temu, wraz z debatą nad samą definicją „100% pokrycia”. Argumenty przeciwko temu mówią, że ostatnie 10-15% nie jest warte wysiłku. Argumenty za tym mówią, że ostatnie 10-15% jest najważniejsze, ponieważ jest tak trudne do przetestowania. Google „hard to unit test bad design”, aby znaleźć przekonujące argumenty, że nietestowalny kod jest oznaką głębszych problemów projektowych.

Where to Go From Here?

Teraz masz kilka świetnych narzędzi do wykorzystania w pisaniu testów dla swoich projektów. Mam nadzieję, że ten samouczek testowania jednostkowego i testowania UI w systemie iOS dał ci pewność, że możesz testować wszystko!

Możesz pobrać ukończoną wersję projektu używając przycisku Pobierz materiały na górze lub na dole tego samouczka. Kontynuuj rozwijanie swoich umiejętności, dodając dodatkowe własne testy.

Oto kilka zasobów do dalszej nauki:

  • Istnieje kilka filmów WWDC na temat testowania. Dwa dobre z WWDC17 to: Engineering for Testability i Testing Tips & Tricks.
  • Następnym krokiem jest automatyzacja: Continuous Integration i Continuous Delivery. Zacznij od Apple’s Automating the Test Process with Xcode Server i xcodebuild, oraz artykułu Wikipedii o ciągłym dostarczaniu, który czerpie z wiedzy ThoughtWorks.
  • Jeśli masz już aplikację, ale jeszcze nie napisałeś dla niej testów, możesz chcieć odwołać się do Working Effectively with Legacy Code Michaela Feathersa, ponieważ kod bez testów jest kodem starości!
  • Jon Reid’s Quality Coding przykładowe archiwa aplikacji są świetne do nauki więcej o Test Driven Development.

raywenderlich.com Weekly

Biuletyn raywenderlich.com to najprostszy sposób, aby być na bieżąco ze wszystkim, co musisz wiedzieć jako programista mobilny.

Zdobądź cotygodniowy przegląd naszych samouczków i kursów, a jako bonus otrzymasz darmowy, dogłębny kurs e-mailowy!

Średnia ocena

4.7/5

Dodaj ocenę dla tej zawartości

Zaloguj się, aby dodać ocenę

90 ocen

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.