În calitate de dezvoltator bun, faceți tot posibilul pentru a testa toate funcționalitățile, fiecare cale posibilă și rezultatul codului în software-ul pe care îl scrieți. Dar este extrem de rar și neobișnuit să poți testa manual fiecare rezultat posibil și orice cale posibilă pe care o poate lua un utilizator.
Pe măsură ce aplicația devine mai mare și mai complexă, probabilitatea că veți pierde ceva prin testarea manuală crește semnificativ.
Testarea automată, atât a interfeței de utilizare, cât și a API-urilor de servicii back-end, vă va face mai încrezători că totul funcționează conform intenției și va reduce stresul atunci când dezvoltați, refactorizați, adăugați funcții noi sau modificați cele existente.
Cu testele automate, puteți:
Acest articol vă învață cum să structurați și să executați teste automate pe platforma iOS.
Este important să faceți diferența între testele de unitate și testele de interfață.
LA test de unitate teste a funcție specifică sub un context specific . Testele unitare verifică dacă partea testată a codului (de obicei o singură funcție) face ceea ce ar trebui să facă. Există o mulțime de cărți și articole despre teste unitare , deci nu vom acoperi acest lucru în această postare.
Teste UI sunt pentru testarea interfeței cu utilizatorul. De exemplu, vă permite să testați dacă o vizualizare este actualizată conform intenției sau dacă o acțiune specifică este declanșată așa cum ar trebui să fie atunci când utilizatorul interacționează cu un anumit element de interfață.
Fiecare test UI testează a interacțiunea specifică a utilizatorului cu interfața de utilizare a aplicației. Testarea automată poate și trebuie să fie efectuată atât la nivelul testului unitar, cât și la nivelul testului UI.
Deoarece XCode acceptă testarea unităților și a interfeței de utilizare, este ușor și simplu să le adăugați la proiect. Când creați un proiect nou, bifați pur și simplu „Includeți teste de unitate” și „Includeți teste de interfață”.
Când proiectul este creat, două ținte noi vor fi adăugate proiectului dvs. atunci când aceste două opțiuni au fost verificate. Numele țintă noi au „Teste” sau „UITesturi” atașate la sfârșitul numelui.
Asta este. Sunteți gata să scrieți teste automate pentru proiectul dvs.
Dacă aveți deja un proiect existent și doriți să adăugați suportul pentru testarea interfeței și a unității, va trebui să faceți ceva mai mult, dar este, de asemenea, foarte simplu și simplu.
Mergi la Fișier → Nou → Țintă și selectați Pachet de testare a unității iOS pentru teste unitare sau Pachetul de testare a interfeței iOS pentru teste UI.
presa Următorul .
În ecranul cu opțiuni de țintă, puteți lăsa totul așa cum este (dacă aveți mai multe ținte și doriți să testați doar anumite ținte, selectați ținta în meniul derulant Țintă de testat).
presa finalizarea . Repetați acest pas pentru testele UI și veți avea totul pregătit pentru a începe să scrieți teste automate în proiectul dvs. existent.
Înainte de a începe să scriem teste unitare, trebuie să le înțelegem anatomia. Când includeți teste unitare în proiectul dvs., va fi creat un exemplu de clasă de testare. În cazul nostru, va arăta astfel:
import XCTest class TestingIOSTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } func testPerformanceExample() { // This is an example of a performance test case. self.measure { // Put the code you want to measure the time of here. } } }
Cele mai importante metode de înțeles sunt setUp
și tearDown
. setUp
metoda se numește inainte de fiecare metodă de testare, în timp ce tearDown
metoda se numește după fiecare metodă de testare. Dacă executăm teste definite în acest exemplu de clasă de testare, metodele ar rula astfel:
setUp → testExample → tearDown setUp → testPerformanceExample → tearDown
Sfat: Testele se execută apăsând cmd + U, selectând Produs → Testare sau făcând clic și menținând apăsat butonul Run până când apare meniul de opțiuni, apoi selectați Test din meniu.
Dacă doriți să rulați o singură metodă de testare specifică, apăsați pe butonul din stânga numelui metodei (afișat în imaginea de mai jos).
Acum, când aveți totul pregătit pentru scrierea testelor, puteți adăuga un exemplu de clasă și câteva metode de testat.
Adăugați o clasă care va fi responsabilă pentru înregistrarea utilizatorilor. Un utilizator introduce o adresă de e-mail, o parolă și o confirmare a parolei. Clasa noastră de exemplu va valida introducerea, va verifica disponibilitatea adresei de e-mail și va încerca înregistrarea utilizatorului.
Notă: acest exemplu utilizează MVVM (sau Model-View-ViewModel) tipar arhitectural.
MVVM este utilizat deoarece face arhitectura unei aplicații mai curată și mai ușor de testat.
Cu MVVM, este mai ușor să separați logica de afaceri de logica de prezentare, evitând astfel problema masivă a controlerului de vizualizare.
Detaliile despre arhitectura MVVM nu intră în sfera acestui articol, dar puteți citi mai multe despre aceasta în Acest articol .
Să creăm o clasă de mod de vizualizare responsabilă cu înregistrarea utilizatorilor. .
class RegisterationViewModel { var emailAddress: String? { didSet { enableRegistrationAttempt() } } var password: String? { didSet { enableRegistrationAttempt() } } var passwordConfirmation: String? { didSet { enableRegistrationAttempt() } } var registrationEnabled = Dynamic(false) var errorMessage = Dynamic('') var loginSuccessful = Dynamic(false) var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } }
În primul rând, am adăugat câteva proprietăți, proprietăți dinamice și o metodă init.
Nu vă faceți griji cu privire la Dynamic
tip. Face parte din arhitectura MVVM.
Când un Dynamic
valoarea este setată la adevărat, un controler de vizualizare care este legat (conectat) la RegistrationViewModel
va activa butonul de înregistrare. Când loginSuccessful
este setat la adevărat, vizualizarea conectată se va actualiza singură.
Să adăugăm acum câteva metode pentru a verifica validitatea parolei și a formatului de e-mail.
func enableRegistrationAttempt() { registrationEnabled.value = emailValid() && passwordValid() } func emailValid() -> Bool { let emailRegEx = '[A-Z0-9a-z._%+-] [email protected] [A-Za-z0-9.-]+\.[A-Za-z]{2,}' let emailTest = NSPredicate(format:'SELF MATCHES %@', emailRegEx) return emailTest.evaluate(with: emailAddress) } func passwordValid() -> Bool { guard let password = password, let passwordConfirmation = passwordConfirmation else { return false } let isValid = (password == passwordConfirmation) && password.characters.count >= 6 return isValid }
De fiecare dată când utilizatorul tastează ceva în e-mail sau în câmpul de parolă, enableRegistrationAttempt
metoda va verifica dacă un e-mail și o parolă sunt în formatul corect și va activa sau dezactiva butonul de înregistrare prin registrationEnabled
proprietate dinamică.
Pentru a menține exemplul simplu, adăugați două metode simple - una pentru a verifica disponibilitatea unui e-mail și una pentru a încerca înregistrarea cu numele de utilizator și parola date.
func checkEmailAvailability(email: String, withCallback callback: @escaping (Bool?)->(Void)) { networkService.checkEmailAvailability(email: email) { (available, error) in if let _ = error { self.errorMessage.value = 'Our custom error message' } else if !available { self.errorMessage.value = 'Sorry, provided email address is already taken' self.registrationEnabled.value = false callback(available) } } } func attemptUserRegistration() { guard registrationEnabled.value == true else { return } // To keep the example as simple as possible, password won't be hashed guard let emailAddress = emailAddress, let passwordHash = password else { return } networkService.attemptRegistration(forUserEmail: emailAddress, withPasswordHash: passwordHash) { (success, error) in // Handle the response if let _ = error { self.errorMessage.value = 'Our custom error message' } else { self.loginSuccessful.value = true } } }
Aceste două metode utilizează NetworkService pentru a verifica dacă un e-mail este disponibil și pentru a încerca înregistrarea.
Pentru a menține acest exemplu simplu, implementarea NetworkService nu folosește niciun API back-end, ci este doar un butuc care falsifică rezultatele. NetworkService este implementat ca protocol și clasa sa de implementare.
typealias RegistrationAttemptCallback = (_ success: Bool, _ error: NSError?) -> Void typealias EmailAvailabilityCallback = (_ available: Bool, _ error: NSError?) -> Void protocol NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) }
NetworkService este un protocol foarte simplu care conține doar două metode: încercare de înregistrare și metode de verificare a disponibilității prin e-mail. Implementarea protocolului este clasa NetworkServiceImpl.
class NetworkServiceImpl: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } }
Ambele metode așteaptă pur și simplu o perioadă de timp (falsificând întârzierea unei cereri de rețea) și apoi apelează metodele de apelare corespunzătoare.
Sfat: este o bună practică să folosiți protocoale (cunoscute și sub numele de interfețe în alte limbaje de programare). Puteți citi mai multe despre aceasta dacă căutați „principiul programării la interfețe”. De asemenea, veți vedea cum se joacă bine cu testarea unitară.
Acum, când este setat un exemplu, putem scrie teste unitare pentru a acoperi metodele acestei clase.
Creați o nouă clasă de testare pentru modelul nostru de vizualizare. Faceți clic dreapta pe TestingIOSTests
din panoul Navigator proiect, selectați Fișier nou → Clasă de caz de testare a unității și denumiți-l RegistrationViewModelTests
.
Ștergeți testExample
și testPerformanceExample
, deoarece vrem să ne creăm propriile metode de testare.
Deoarece Swift folosește module, iar testele noastre se află într-un modul diferit de codul aplicației noastre, trebuie să importăm modulul aplicației noastre ca @testable
. Sub declarația de import și definiția clasei, adăugați @testable import TestingIOS
(sau numele modulului aplicației dvs.). Fără aceasta, nu am putea face referire la niciuna dintre clasele sau metodele aplicației noastre.
Adăugați registrationViewModel
variabil.
Așa arată acum clasa noastră de testare goală:
import XCTest @testable import TestingIOS class RegistrationViewModelTests: XCTestCase { var registrationViewModel: RegisterationViewModel? override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }
Să încercăm să scriem un test pentru emailValid
metodă. Vom crea o nouă metodă de testare numită testEmailValid
. Este important să adăugați test
cuvânt cheie la începutul numelui. În caz contrar, metoda nu va fi recunoscută ca metodă de testare.
Metoda noastră de testare arată astfel:
func testEmailValid() { let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl()) registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = nil XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssert(registrationVM.emailValid(), '(registrationVM.emailAddress) should be correct') }
Metoda noastră de testare utilizează o metodă de afirmare, XCTAssert
, care, în cazul nostru, verifică dacă o condiție este adevărată sau falsă.
Dacă condiția este falsă, afirmarea va eșua (împreună cu testul), iar mesajul nostru va fi scris.
Există multe metode de afirmare pe care le puteți folosi în testele dvs. Descrierea și afișarea fiecărei metode de afirmare poate crea cu ușurință propriul articol, așa că nu voi intra în detalii aici.
Câteva exemple de metode de afirmare disponibile sunt: XCTAssertEqualObjects
, XCTAssertGreaterThan
, XCTAssertNil
, XCTAssertTrue
sau XCTAssertThrows
.
Puteți citi mai multe despre metodele de afirmare disponibile Aici .
Dacă rulați testul acum, metoda de testare va trece. Ați creat cu succes prima dvs. metodă de testare, dar nu este încă pregătită pentru prime time. Această metodă de testare are încă trei probleme (una mare și două mai mici), așa cum este detaliat mai jos.
Unul dintre principiile de bază ale testării unitare este că fiecare test trebuie să fie independent de orice factor extern sau dependență. Testele unitare ar trebui să fie atomice.
Dacă testați o metodă, care la un moment dat apelează o metodă API de la server, testul dvs. depinde de codul dvs. de rețea și de disponibilitatea serverului. Dacă serverul nu funcționează în momentul testării, testul dvs. va eșua, acuzând astfel în mod greșit metoda testată că nu funcționează.
În acest caz, testați o metodă a RegistrationViewModel
.
RegistrationViewModel
depinde de NetworkServiceImpl
clasa, chiar dacă știți că metoda dvs. testată, emailValid
, nu depinde de NetworkServiceImpl
direct.
Când scrieți teste unitare, toate dependențele externe ar trebui eliminate. Dar cum ar trebui să eliminați dependența NetworkService fără a schimba implementarea RegistrationViewModel
clasă?
Există o soluție ușoară la această problemă și se numește Batjocorirea obiectelor . Dacă vă uitați atent la RegistrationViewModel
, veți vedea că depinde de fapt de NetworkService
protocol.
class RegisterationViewModel { … // It depends on NetworkService. RegistrationViewModel doesn't even care if NetworkServiceImple exists var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } ...
Când RegistrationViewModel
este inițializată, o implementare a NetworkService
protocolul este dat (sau injectat) către RegistrationViewModel
obiect.
Acest principiu se numește injectarea dependenței prin constructor ( există mai multe tipuri de injecții de dependență ).
Există o mulțime de articole interesante despre injectarea dependenței online, cum ar fi acest articol despre objc.io .
Există, de asemenea, un articol scurt, dar interesant, care explică injecția dependenței într-un mod simplu și direct Aici .
În plus, un articol minunat despre principiul responsabilității unice și DI este disponibil pe Blogul ApeeScape .
Când RegistrationViewModel
este instanțiat, injectează o implementare a protocolului NetworkService în constructorul său (de aici și numele principiului injecției dependenței):
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
Deoarece clasa modelului nostru de vizualizare depinde doar de protocol, nimic nu ne împiedică să ne creăm personalizat (sau batjocorit) NetworkService
implementarea clasei și injectarea clasei batjocorite în obiectul nostru model de vizualizare.
Să creăm batjocorul nostru NetworkService
implementarea protocolului.
Adăugați un nou fișier Swift la ținta noastră de testare făcând clic dreapta pe TestingIOSTests
din Project Navigator, alegeți „New File”, selectați „Swift file” și denumiți-l NetworkServiceMock
.
Așa ar trebui să arate clasa noastră batjocorită:
import Foundation @testable import TestingIOS class NetworkServiceMock: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(false, nil) }) } }
În acest moment, nu este mult diferit de implementarea noastră reală (NetworkServiceImpl
), dar într-o situație reală, actualul NetworkServiceImpl
ar avea un cod de rețea, gestionarea răspunsului și o funcționalitate similară.
Clasa noastră batjocorită nu face nimic, ceea ce este punctul unei clase batjocorite. Dacă nu face nimic, nu va interfera cu testele noastre.
Pentru a remedia prima problemă a testului, să actualizăm metoda de testare înlocuind:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
cu:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())
Există setUp
și tearDown
metode pentru un motiv.
Aceste metode sunt folosite pentru a iniția sau configura toate obiectele necesare necesare unui test. Ar trebui să utilizați aceste metode pentru a evita duplicarea codului, scriind aceleași metode inițiale sau de configurare în fiecare metodă de testare. Neutilizarea metodelor de configurare și tearDown nu este întotdeauna o problemă importantă, mai ales dacă aveți o configurație cu adevărat specifică pentru o metodă de testare specifică.
De la inițializarea noastră a RegistrationViewModel
clasa este destul de simplă, vă veți refactura clasa de testare pentru a utiliza metodele de configurare și demolare.
RegistrationViewModelTests
ar trebui să arate așa:
class RegistrationViewModelTests: XCTestCase { var registrationVM: RegisterationViewModel! override func setUp() { super.setUp() registrationVM = RegisterationViewModel(networkService: NetworkServiceMock()) } override func tearDown() { registrationVM = nil super.tearDown() } func testEmailValid() { registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') ... } }
Chiar dacă aceasta nu este o problemă importantă, există unii susținători ai unei afirmări pe metodă.
Principalul raționament pentru acest principiu este detectarea erorilor.
Dacă o metodă de testare are mai multe afirmări și prima eșuează, întreaga metodă de testare va fi marcată ca eșuată. Alte afirmații nici măcar nu vor fi testate.
În acest fel, veți descoperi o singură eroare la un moment dat. Nu ați ști dacă alte afirmații vor eșua sau vor reuși.
Nu este întotdeauna un lucru rău să ai mai multe afirmări într-o singură metodă, deoarece poți remedia o singură eroare odată, deci detectarea unei erori la un moment dat ar putea să nu fie atât de mare ca o problemă.
În cazul nostru, se testează validitatea unui format de e-mail. Deoarece aceasta este doar o funcție, ar putea fi mai logic să grupați toate afirmările într-o singură metodă pentru a face testul mai ușor de citit și de înțeles.
Deoarece această problemă nu este de fapt o problemă importantă și unii ar putea chiar susține că nu este deloc o problemă, vă veți păstra metoda de testare așa cum este.
Când scrieți propriile teste unitare, depinde de dvs. să decideți ce cale doriți să luați pentru fiecare metodă de testare. Cel mai probabil, veți găsi că există locuri în care cel care afirmă pe fiecare filozofie de test are sens, iar altele în care nu.
Indiferent cât de simplă este aplicația, există șanse mari să existe o metodă care trebuie executată pe un alt fir în mod asincron, mai ales că în mod obișnuit doriți să aveți interfața de utilizare care se execută în propriul fir.
Problema principală cu testarea unității și apelurile asincrone este că un apel asincron durează timp pentru a se termina, dar testul unitar nu va aștepta până când se termină. Deoarece testul unitar este terminat înainte de executarea oricărui cod din interiorul unui bloc asincron, testul nostru se va încheia întotdeauna cu același rezultat (indiferent de ce scrieți în blocul asincron).
Pentru a demonstra acest lucru, să creăm un test pentru checkEmailAvailability
metodă.
func testCheckEmailAvailability() { registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled') } }
Aici doriți să testați dacă o variabilă de înregistrareEnabled va fi setată la fals după ce metoda noastră vă spune că e-mailul nu este disponibil (deja preluat de un alt utilizator).
Dacă rulați acest test, acesta va trece. Dar încearcă încă un lucru. Schimbați afirmarea în:
XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled')
Dacă rulați din nou testul, acesta trece din nou.
Acest lucru se datorează faptului că afirmația noastră nici măcar nu a fost afirmată. Testul unitar s-a încheiat înainte de executarea blocului de apel invers (amintiți-vă, în implementarea serviciului nostru de rețea batjocorit, este setat să aștepte o secundă înainte de a reveni).
Din fericire, cu Xcode 6, Apple a adăugat așteptări de testare în cadrul XCTest ca XCTestExpectation
clasă. XCTestExpectation
clasa funcționează astfel:
waitForExpectationWithTimer
bloc. Acesta va fi executat atunci când așteptările sunt îndeplinite sau dacă temporizatorul se epuizează - oricare se întâmplă mai întâi.Să rescriem testul nostru pentru a folosi XCTestExpectation
clasă.
func testCheckEmailAvailability() { // 1. Setting the expectation let exp = expectation(description: 'Check email availability') registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled') // 2. Fulfilling the expectation exp.fulfill() } // 3. Waiting for expectation to fulfill waitForExpectations(timeout: 3.0) { error in if let _ = error { XCTAssert(false, 'Timeout while checking email availability') } } }
Dacă rulați testul acum, acesta va eșua - așa cum ar trebui. Să remediem testul pentru a-l trece. Schimbați afirmarea în:
XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled')
Rulați din nou testul pentru a vedea cum trece. Puteți încerca să schimbați timpul de întârziere în implementarea batjocorită a serviciului de rețea pentru a vedea ce se întâmplă dacă expiră temporizatorul.
Exemplul nostru de metodă de proiect attemptUserRegistration
folosește NetworkService.attemptRegistration
metodă care include cod care se execută asincron. Metoda încearcă să înregistreze un utilizator cu serviciul backend.
În aplicația noastră demonstrativă, metoda va aștepta doar o secundă pentru a simula un apel de rețea și va înregistra cu succes fals. Dacă înregistrarea a avut succes, loginSuccessful
valoarea va fi setată la adevărat. Să facem un test unitar pentru a verifica acest comportament.
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.attemptUserRegistration() XCTAssert(registrationVM.loginSuccessful.value, 'Login must be successful') }
Dacă se execută, acest test nu va reuși deoarece loginSuccessful
valoarea nu va fi setată la adevărat până la asincron networkService.attemptRegistration
metoda este terminată.
De când ați creat un joc batjocorit NetworkServiceImpl
unde attemptRegistration
metoda va aștepta o secundă înainte de a returna o înregistrare reușită, puteți utiliza doar Grand Central Dispatch (GCD) și utilizați asyncAfter
metoda de verificare a afirmării după o secundă. După adăugarea GCD’s asyncAfter
codul nostru de testare va arăta astfel:
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') } }
Dacă ați acordat atenție, veți ști că acest lucru încă nu va funcționa, deoarece se va executa metoda de testare inainte de | | + _ | blocul este executat și metoda va mereu trece cu succes ca rezultat. Din fericire, există asyncAfter
clasă.
Să rescriem metoda noastră de a folosi XCTestException
clasă:
XCTestException
Cu testele unitare care acoperă func testAttemptRegistration() { let exp = expectation(description: 'Check registration attempt') registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') exp.fulfill() } waitForExpectations(timeout: 4.0) { error in if let _ = error { XCTAssert(false, 'Timeout while attempting a registration') } } }
, puteți fi acum mai siguri că adăugarea funcționalității noi sau actualizarea existentă nu va sparge nimic.
Notă importantă: Testele unitare își vor pierde valoarea dacă nu sunt actualizate atunci când funcționalitatea metodelor pe care le acoperă se modifică. Scrierea testelor unitare este un proces care trebuie să țină pasul cu restul aplicației.
Sfat: nu amâna testele de scriere până la final. Scrieți teste în timp ce vă dezvoltați. În acest fel veți avea o mai bună înțelegere a ceea ce trebuie testat și care sunt cazurile la frontieră.
După ce toate testele unitare sunt complet dezvoltate și executate cu succes, puteți fi foarte încrezător că fiecare unitate de cod funcționează corect, dar înseamnă că aplicația dvs. în ansamblu funcționează conform intenției?
Aici intervin testele de integrare, dintre care testele UI sunt o componentă esențială.
Înainte de a începe cu testarea UI, trebuie să existe unele elemente UI și interacțiuni (sau povești ale utilizatorilor) de testat. Să creăm o vizualizare simplă și un controler de vizualizare.
RegistrationViewModel
și creați un controler de vizualizare simplu, care va arăta ca cel din imaginea de mai jos.
Setați eticheta câmpului de text al e-mailului la 100, eticheta câmpului de text al parolei la 101 și eticheta de confirmare a parolei la 102.
Main.storyboard
și conectați toate punctele de vânzare cu storyboard-ul.RegistrationViewController.swift
Aici adăugați import UIKit class RegistrationViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var passwordConfirmationTextField: UITextField! @IBOutlet weak var registerButton: UIButton! private struct TextFieldTags { static let emailTextField = 100 static let passwordTextField = 101 static let confirmPasswordTextField = 102 } var viewModel: RegisterationViewModel? override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self passwordConfirmationTextField.delegate = self bindViewModel() } }
și un IBOutlets
struct la clasă.
Acest lucru vă va permite să identificați câmpul de text care este editat. Pentru a utiliza proprietățile dinamice în modelul de vizualizare, trebuie să „legați” proprietățile dinamice în controlerul de vizualizare. Puteți face acest lucru în TextFieldTags
metodă:
bindViewModel
Să adăugăm acum o metodă de delegare a câmpului de text pentru a urmări când se actualizează oricare dintre câmpurile de text:
fileprivate func bindViewModel() { if let viewModel = viewModel { viewModel.registrationEnabled.bindAndFire { self.registerButton.isEnabled = Cum se scrie teste automate pentru iOS
În calitate de dezvoltator bun, faceți tot posibilul pentru a testa toate funcționalitățile, fiecare cale posibilă și rezultatul codului în software-ul pe care îl scrieți. Dar este extrem de rar și neobișnuit să poți testa manual fiecare rezultat posibil și orice cale posibilă pe care o poate lua un utilizator.
Pe măsură ce aplicația devine mai mare și mai complexă, probabilitatea că veți pierde ceva prin testarea manuală crește semnificativ.
Testarea automată, atât a interfeței de utilizare, cât și a API-urilor de servicii back-end, vă va face mai încrezători că totul funcționează conform intenției și va reduce stresul atunci când dezvoltați, refactorizați, adăugați funcții noi sau modificați cele existente.
Cu testele automate, puteți:
Acest articol vă învață cum să structurați și să executați teste automate pe platforma iOS.
Este important să faceți diferența între testele de unitate și testele de interfață.
LA test de unitate teste a funcție specifică sub un context specific . Testele unitare verifică dacă partea testată a codului (de obicei o singură funcție) face ceea ce ar trebui să facă. Există o mulțime de cărți și articole despre teste unitare , deci nu vom acoperi acest lucru în această postare.
Teste UI sunt pentru testarea interfeței cu utilizatorul. De exemplu, vă permite să testați dacă o vizualizare este actualizată conform intenției sau dacă o acțiune specifică este declanșată așa cum ar trebui să fie atunci când utilizatorul interacționează cu un anumit element de interfață.
Fiecare test UI testează a interacțiunea specifică a utilizatorului cu interfața de utilizare a aplicației. Testarea automată poate și trebuie să fie efectuată atât la nivelul testului unitar, cât și la nivelul testului UI.
Deoarece XCode acceptă testarea unităților și a interfeței de utilizare, este ușor și simplu să le adăugați la proiect. Când creați un proiect nou, bifați pur și simplu „Includeți teste de unitate” și „Includeți teste de interfață”.
Când proiectul este creat, două ținte noi vor fi adăugate proiectului dvs. atunci când aceste două opțiuni au fost verificate. Numele țintă noi au „Teste” sau „UITesturi” atașate la sfârșitul numelui.
Asta este. Sunteți gata să scrieți teste automate pentru proiectul dvs.
Dacă aveți deja un proiect existent și doriți să adăugați suportul pentru testarea interfeței și a unității, va trebui să faceți ceva mai mult, dar este, de asemenea, foarte simplu și simplu.
Mergi la Fișier → Nou → Țintă și selectați Pachet de testare a unității iOS pentru teste unitare sau Pachetul de testare a interfeței iOS pentru teste UI.
presa Următorul .
În ecranul cu opțiuni de țintă, puteți lăsa totul așa cum este (dacă aveți mai multe ținte și doriți să testați doar anumite ținte, selectați ținta în meniul derulant Țintă de testat).
presa finalizarea . Repetați acest pas pentru testele UI și veți avea totul pregătit pentru a începe să scrieți teste automate în proiectul dvs. existent.
Înainte de a începe să scriem teste unitare, trebuie să le înțelegem anatomia. Când includeți teste unitare în proiectul dvs., va fi creat un exemplu de clasă de testare. În cazul nostru, va arăta astfel:
import XCTest class TestingIOSTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } func testPerformanceExample() { // This is an example of a performance test case. self.measure { // Put the code you want to measure the time of here. } } }
Cele mai importante metode de înțeles sunt setUp
și tearDown
. setUp
metoda se numește inainte de fiecare metodă de testare, în timp ce tearDown
metoda se numește după fiecare metodă de testare. Dacă executăm teste definite în acest exemplu de clasă de testare, metodele ar rula astfel:
setUp → testExample → tearDown setUp → testPerformanceExample → tearDown
Sfat: Testele se execută apăsând cmd + U, selectând Produs → Testare sau făcând clic și menținând apăsat butonul Run până când apare meniul de opțiuni, apoi selectați Test din meniu.
Dacă doriți să rulați o singură metodă de testare specifică, apăsați pe butonul din stânga numelui metodei (afișat în imaginea de mai jos).
Acum, când aveți totul pregătit pentru scrierea testelor, puteți adăuga un exemplu de clasă și câteva metode de testat.
Adăugați o clasă care va fi responsabilă pentru înregistrarea utilizatorilor. Un utilizator introduce o adresă de e-mail, o parolă și o confirmare a parolei. Clasa noastră de exemplu va valida introducerea, va verifica disponibilitatea adresei de e-mail și va încerca înregistrarea utilizatorului.
Notă: acest exemplu utilizează MVVM (sau Model-View-ViewModel) tipar arhitectural.
MVVM este utilizat deoarece face arhitectura unei aplicații mai curată și mai ușor de testat.
Cu MVVM, este mai ușor să separați logica de afaceri de logica de prezentare, evitând astfel problema masivă a controlerului de vizualizare.
Detaliile despre arhitectura MVVM nu intră în sfera acestui articol, dar puteți citi mai multe despre aceasta în Acest articol .
Să creăm o clasă de mod de vizualizare responsabilă cu înregistrarea utilizatorilor. .
class RegisterationViewModel { var emailAddress: String? { didSet { enableRegistrationAttempt() } } var password: String? { didSet { enableRegistrationAttempt() } } var passwordConfirmation: String? { didSet { enableRegistrationAttempt() } } var registrationEnabled = Dynamic(false) var errorMessage = Dynamic('') var loginSuccessful = Dynamic(false) var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } }
În primul rând, am adăugat câteva proprietăți, proprietăți dinamice și o metodă init.
Nu vă faceți griji cu privire la Dynamic
tip. Face parte din arhitectura MVVM.
Când un Dynamic
valoarea este setată la adevărat, un controler de vizualizare care este legat (conectat) la RegistrationViewModel
va activa butonul de înregistrare. Când loginSuccessful
este setat la adevărat, vizualizarea conectată se va actualiza singură.
Să adăugăm acum câteva metode pentru a verifica validitatea parolei și a formatului de e-mail.
func enableRegistrationAttempt() { registrationEnabled.value = emailValid() && passwordValid() } func emailValid() -> Bool { let emailRegEx = '[A-Z0-9a-z._%+-] [email protected] [A-Za-z0-9.-]+\.[A-Za-z]{2,}' let emailTest = NSPredicate(format:'SELF MATCHES %@', emailRegEx) return emailTest.evaluate(with: emailAddress) } func passwordValid() -> Bool { guard let password = password, let passwordConfirmation = passwordConfirmation else { return false } let isValid = (password == passwordConfirmation) && password.characters.count >= 6 return isValid }
De fiecare dată când utilizatorul tastează ceva în e-mail sau în câmpul de parolă, enableRegistrationAttempt
metoda va verifica dacă un e-mail și o parolă sunt în formatul corect și va activa sau dezactiva butonul de înregistrare prin registrationEnabled
proprietate dinamică.
Pentru a menține exemplul simplu, adăugați două metode simple - una pentru a verifica disponibilitatea unui e-mail și una pentru a încerca înregistrarea cu numele de utilizator și parola date.
func checkEmailAvailability(email: String, withCallback callback: @escaping (Bool?)->(Void)) { networkService.checkEmailAvailability(email: email) { (available, error) in if let _ = error { self.errorMessage.value = 'Our custom error message' } else if !available { self.errorMessage.value = 'Sorry, provided email address is already taken' self.registrationEnabled.value = false callback(available) } } } func attemptUserRegistration() { guard registrationEnabled.value == true else { return } // To keep the example as simple as possible, password won't be hashed guard let emailAddress = emailAddress, let passwordHash = password else { return } networkService.attemptRegistration(forUserEmail: emailAddress, withPasswordHash: passwordHash) { (success, error) in // Handle the response if let _ = error { self.errorMessage.value = 'Our custom error message' } else { self.loginSuccessful.value = true } } }
Aceste două metode utilizează NetworkService pentru a verifica dacă un e-mail este disponibil și pentru a încerca înregistrarea.
Pentru a menține acest exemplu simplu, implementarea NetworkService nu folosește niciun API back-end, ci este doar un butuc care falsifică rezultatele. NetworkService este implementat ca protocol și clasa sa de implementare.
typealias RegistrationAttemptCallback = (_ success: Bool, _ error: NSError?) -> Void typealias EmailAvailabilityCallback = (_ available: Bool, _ error: NSError?) -> Void protocol NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) }
NetworkService este un protocol foarte simplu care conține doar două metode: încercare de înregistrare și metode de verificare a disponibilității prin e-mail. Implementarea protocolului este clasa NetworkServiceImpl.
class NetworkServiceImpl: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } }
Ambele metode așteaptă pur și simplu o perioadă de timp (falsificând întârzierea unei cereri de rețea) și apoi apelează metodele de apelare corespunzătoare.
Sfat: este o bună practică să folosiți protocoale (cunoscute și sub numele de interfețe în alte limbaje de programare). Puteți citi mai multe despre aceasta dacă căutați „principiul programării la interfețe”. De asemenea, veți vedea cum se joacă bine cu testarea unitară.
Acum, când este setat un exemplu, putem scrie teste unitare pentru a acoperi metodele acestei clase.
Creați o nouă clasă de testare pentru modelul nostru de vizualizare. Faceți clic dreapta pe TestingIOSTests
din panoul Navigator proiect, selectați Fișier nou → Clasă de caz de testare a unității și denumiți-l RegistrationViewModelTests
.
Ștergeți testExample
și testPerformanceExample
, deoarece vrem să ne creăm propriile metode de testare.
Deoarece Swift folosește module, iar testele noastre se află într-un modul diferit de codul aplicației noastre, trebuie să importăm modulul aplicației noastre ca @testable
. Sub declarația de import și definiția clasei, adăugați @testable import TestingIOS
(sau numele modulului aplicației dvs.). Fără aceasta, nu am putea face referire la niciuna dintre clasele sau metodele aplicației noastre.
Adăugați registrationViewModel
variabil.
Așa arată acum clasa noastră de testare goală:
import XCTest @testable import TestingIOS class RegistrationViewModelTests: XCTestCase { var registrationViewModel: RegisterationViewModel? override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }
Să încercăm să scriem un test pentru emailValid
metodă. Vom crea o nouă metodă de testare numită testEmailValid
. Este important să adăugați test
cuvânt cheie la începutul numelui. În caz contrar, metoda nu va fi recunoscută ca metodă de testare.
Metoda noastră de testare arată astfel:
func testEmailValid() { let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl()) registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = nil XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssert(registrationVM.emailValid(), '(registrationVM.emailAddress) should be correct') }
Metoda noastră de testare utilizează o metodă de afirmare, XCTAssert
, care, în cazul nostru, verifică dacă o condiție este adevărată sau falsă.
Dacă condiția este falsă, afirmarea va eșua (împreună cu testul), iar mesajul nostru va fi scris.
Există multe metode de afirmare pe care le puteți folosi în testele dvs. Descrierea și afișarea fiecărei metode de afirmare poate crea cu ușurință propriul articol, așa că nu voi intra în detalii aici.
Câteva exemple de metode de afirmare disponibile sunt: XCTAssertEqualObjects
, XCTAssertGreaterThan
, XCTAssertNil
, XCTAssertTrue
sau XCTAssertThrows
.
Puteți citi mai multe despre metodele de afirmare disponibile Aici .
Dacă rulați testul acum, metoda de testare va trece. Ați creat cu succes prima dvs. metodă de testare, dar nu este încă pregătită pentru prime time. Această metodă de testare are încă trei probleme (una mare și două mai mici), așa cum este detaliat mai jos.
Unul dintre principiile de bază ale testării unitare este că fiecare test trebuie să fie independent de orice factor extern sau dependență. Testele unitare ar trebui să fie atomice.
Dacă testați o metodă, care la un moment dat apelează o metodă API de la server, testul dvs. depinde de codul dvs. de rețea și de disponibilitatea serverului. Dacă serverul nu funcționează în momentul testării, testul dvs. va eșua, acuzând astfel în mod greșit metoda testată că nu funcționează.
În acest caz, testați o metodă a RegistrationViewModel
.
RegistrationViewModel
depinde de NetworkServiceImpl
clasa, chiar dacă știți că metoda dvs. testată, emailValid
, nu depinde de NetworkServiceImpl
direct.
Când scrieți teste unitare, toate dependențele externe ar trebui eliminate. Dar cum ar trebui să eliminați dependența NetworkService fără a schimba implementarea RegistrationViewModel
clasă?
Există o soluție ușoară la această problemă și se numește Batjocorirea obiectelor . Dacă vă uitați atent la RegistrationViewModel
, veți vedea că depinde de fapt de NetworkService
protocol.
class RegisterationViewModel { … // It depends on NetworkService. RegistrationViewModel doesn't even care if NetworkServiceImple exists var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } ...
Când RegistrationViewModel
este inițializată, o implementare a NetworkService
protocolul este dat (sau injectat) către RegistrationViewModel
obiect.
Acest principiu se numește injectarea dependenței prin constructor ( există mai multe tipuri de injecții de dependență ).
Există o mulțime de articole interesante despre injectarea dependenței online, cum ar fi acest articol despre objc.io .
Există, de asemenea, un articol scurt, dar interesant, care explică injecția dependenței într-un mod simplu și direct Aici .
În plus, un articol minunat despre principiul responsabilității unice și DI este disponibil pe Blogul ApeeScape .
Când RegistrationViewModel
este instanțiat, injectează o implementare a protocolului NetworkService în constructorul său (de aici și numele principiului injecției dependenței):
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
Deoarece clasa modelului nostru de vizualizare depinde doar de protocol, nimic nu ne împiedică să ne creăm personalizat (sau batjocorit) NetworkService
implementarea clasei și injectarea clasei batjocorite în obiectul nostru model de vizualizare.
Să creăm batjocorul nostru NetworkService
implementarea protocolului.
Adăugați un nou fișier Swift la ținta noastră de testare făcând clic dreapta pe TestingIOSTests
din Project Navigator, alegeți „New File”, selectați „Swift file” și denumiți-l NetworkServiceMock
.
Așa ar trebui să arate clasa noastră batjocorită:
import Foundation @testable import TestingIOS class NetworkServiceMock: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(false, nil) }) } }
În acest moment, nu este mult diferit de implementarea noastră reală (NetworkServiceImpl
), dar într-o situație reală, actualul NetworkServiceImpl
ar avea un cod de rețea, gestionarea răspunsului și o funcționalitate similară.
Clasa noastră batjocorită nu face nimic, ceea ce este punctul unei clase batjocorite. Dacă nu face nimic, nu va interfera cu testele noastre.
Pentru a remedia prima problemă a testului, să actualizăm metoda de testare înlocuind:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
cu:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())
Există setUp
și tearDown
metode pentru un motiv.
Aceste metode sunt folosite pentru a iniția sau configura toate obiectele necesare necesare unui test. Ar trebui să utilizați aceste metode pentru a evita duplicarea codului, scriind aceleași metode inițiale sau de configurare în fiecare metodă de testare. Neutilizarea metodelor de configurare și tearDown nu este întotdeauna o problemă importantă, mai ales dacă aveți o configurație cu adevărat specifică pentru o metodă de testare specifică.
De la inițializarea noastră a RegistrationViewModel
clasa este destul de simplă, vă veți refactura clasa de testare pentru a utiliza metodele de configurare și demolare.
RegistrationViewModelTests
ar trebui să arate așa:
class RegistrationViewModelTests: XCTestCase { var registrationVM: RegisterationViewModel! override func setUp() { super.setUp() registrationVM = RegisterationViewModel(networkService: NetworkServiceMock()) } override func tearDown() { registrationVM = nil super.tearDown() } func testEmailValid() { registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') ... } }
Chiar dacă aceasta nu este o problemă importantă, există unii susținători ai unei afirmări pe metodă.
Principalul raționament pentru acest principiu este detectarea erorilor.
Dacă o metodă de testare are mai multe afirmări și prima eșuează, întreaga metodă de testare va fi marcată ca eșuată. Alte afirmații nici măcar nu vor fi testate.
În acest fel, veți descoperi o singură eroare la un moment dat. Nu ați ști dacă alte afirmații vor eșua sau vor reuși.
Nu este întotdeauna un lucru rău să ai mai multe afirmări într-o singură metodă, deoarece poți remedia o singură eroare odată, deci detectarea unei erori la un moment dat ar putea să nu fie atât de mare ca o problemă.
În cazul nostru, se testează validitatea unui format de e-mail. Deoarece aceasta este doar o funcție, ar putea fi mai logic să grupați toate afirmările într-o singură metodă pentru a face testul mai ușor de citit și de înțeles.
Deoarece această problemă nu este de fapt o problemă importantă și unii ar putea chiar susține că nu este deloc o problemă, vă veți păstra metoda de testare așa cum este.
Când scrieți propriile teste unitare, depinde de dvs. să decideți ce cale doriți să luați pentru fiecare metodă de testare. Cel mai probabil, veți găsi că există locuri în care cel care afirmă pe fiecare filozofie de test are sens, iar altele în care nu.
Indiferent cât de simplă este aplicația, există șanse mari să existe o metodă care trebuie executată pe un alt fir în mod asincron, mai ales că în mod obișnuit doriți să aveți interfața de utilizare care se execută în propriul fir.
Problema principală cu testarea unității și apelurile asincrone este că un apel asincron durează timp pentru a se termina, dar testul unitar nu va aștepta până când se termină. Deoarece testul unitar este terminat înainte de executarea oricărui cod din interiorul unui bloc asincron, testul nostru se va încheia întotdeauna cu același rezultat (indiferent de ce scrieți în blocul asincron).
Pentru a demonstra acest lucru, să creăm un test pentru checkEmailAvailability
metodă.
func testCheckEmailAvailability() { registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled') } }
Aici doriți să testați dacă o variabilă de înregistrareEnabled va fi setată la fals după ce metoda noastră vă spune că e-mailul nu este disponibil (deja preluat de un alt utilizator).
Dacă rulați acest test, acesta va trece. Dar încearcă încă un lucru. Schimbați afirmarea în:
XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled')
Dacă rulați din nou testul, acesta trece din nou.
Acest lucru se datorează faptului că afirmația noastră nici măcar nu a fost afirmată. Testul unitar s-a încheiat înainte de executarea blocului de apel invers (amintiți-vă, în implementarea serviciului nostru de rețea batjocorit, este setat să aștepte o secundă înainte de a reveni).
Din fericire, cu Xcode 6, Apple a adăugat așteptări de testare în cadrul XCTest ca XCTestExpectation
clasă. XCTestExpectation
clasa funcționează astfel:
waitForExpectationWithTimer
bloc. Acesta va fi executat atunci când așteptările sunt îndeplinite sau dacă temporizatorul se epuizează - oricare se întâmplă mai întâi.Să rescriem testul nostru pentru a folosi XCTestExpectation
clasă.
func testCheckEmailAvailability() { // 1. Setting the expectation let exp = expectation(description: 'Check email availability') registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled') // 2. Fulfilling the expectation exp.fulfill() } // 3. Waiting for expectation to fulfill waitForExpectations(timeout: 3.0) { error in if let _ = error { XCTAssert(false, 'Timeout while checking email availability') } } }
Dacă rulați testul acum, acesta va eșua - așa cum ar trebui. Să remediem testul pentru a-l trece. Schimbați afirmarea în:
XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled')
Rulați din nou testul pentru a vedea cum trece. Puteți încerca să schimbați timpul de întârziere în implementarea batjocorită a serviciului de rețea pentru a vedea ce se întâmplă dacă expiră temporizatorul.
Exemplul nostru de metodă de proiect attemptUserRegistration
folosește NetworkService.attemptRegistration
metodă care include cod care se execută asincron. Metoda încearcă să înregistreze un utilizator cu serviciul backend.
În aplicația noastră demonstrativă, metoda va aștepta doar o secundă pentru a simula un apel de rețea și va înregistra cu succes fals. Dacă înregistrarea a avut succes, loginSuccessful
valoarea va fi setată la adevărat. Să facem un test unitar pentru a verifica acest comportament.
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.attemptUserRegistration() XCTAssert(registrationVM.loginSuccessful.value, 'Login must be successful') }
Dacă se execută, acest test nu va reuși deoarece loginSuccessful
valoarea nu va fi setată la adevărat până la asincron networkService.attemptRegistration
metoda este terminată.
De când ați creat un joc batjocorit NetworkServiceImpl
unde attemptRegistration
metoda va aștepta o secundă înainte de a returna o înregistrare reușită, puteți utiliza doar Grand Central Dispatch (GCD) și utilizați asyncAfter
metoda de verificare a afirmării după o secundă. După adăugarea GCD’s asyncAfter
codul nostru de testare va arăta astfel:
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') } }
Dacă ați acordat atenție, veți ști că acest lucru încă nu va funcționa, deoarece se va executa metoda de testare inainte de | | + _ | blocul este executat și metoda va mereu trece cu succes ca rezultat. Din fericire, există asyncAfter
clasă.
Să rescriem metoda noastră de a folosi XCTestException
clasă:
XCTestException
Cu testele unitare care acoperă func testAttemptRegistration() { let exp = expectation(description: 'Check registration attempt') registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') exp.fulfill() } waitForExpectations(timeout: 4.0) { error in if let _ = error { XCTAssert(false, 'Timeout while attempting a registration') } } }
, puteți fi acum mai siguri că adăugarea funcționalității noi sau actualizarea existentă nu va sparge nimic.
Notă importantă: Testele unitare își vor pierde valoarea dacă nu sunt actualizate atunci când funcționalitatea metodelor pe care le acoperă se modifică. Scrierea testelor unitare este un proces care trebuie să țină pasul cu restul aplicației.
Sfat: nu amâna testele de scriere până la final. Scrieți teste în timp ce vă dezvoltați. În acest fel veți avea o mai bună înțelegere a ceea ce trebuie testat și care sunt cazurile la frontieră.
După ce toate testele unitare sunt complet dezvoltate și executate cu succes, puteți fi foarte încrezător că fiecare unitate de cod funcționează corect, dar înseamnă că aplicația dvs. în ansamblu funcționează conform intenției?
Aici intervin testele de integrare, dintre care testele UI sunt o componentă esențială.
Înainte de a începe cu testarea UI, trebuie să existe unele elemente UI și interacțiuni (sau povești ale utilizatorilor) de testat. Să creăm o vizualizare simplă și un controler de vizualizare.
RegistrationViewModel
și creați un controler de vizualizare simplu, care va arăta ca cel din imaginea de mai jos.
Setați eticheta câmpului de text al e-mailului la 100, eticheta câmpului de text al parolei la 101 și eticheta de confirmare a parolei la 102.
Main.storyboard
și conectați toate punctele de vânzare cu storyboard-ul.RegistrationViewController.swift
Aici adăugați import UIKit class RegistrationViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var passwordConfirmationTextField: UITextField! @IBOutlet weak var registerButton: UIButton! private struct TextFieldTags { static let emailTextField = 100 static let passwordTextField = 101 static let confirmPasswordTextField = 102 } var viewModel: RegisterationViewModel? override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self passwordConfirmationTextField.delegate = self bindViewModel() } }
și un IBOutlets
struct la clasă.
Acest lucru vă va permite să identificați câmpul de text care este editat. Pentru a utiliza proprietățile dinamice în modelul de vizualizare, trebuie să „legați” proprietățile dinamice în controlerul de vizualizare. Puteți face acest lucru în TextFieldTags
metodă:
bindViewModel
Să adăugăm acum o metodă de delegare a câmpului de text pentru a urmări când se actualizează oricare dintre câmpurile de text:
fileprivate func bindViewModel() { if let viewModel = viewModel { viewModel.registrationEnabled.bindAndFire { self.registerButton.isEnabled = $0 } } }
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let viewModel = viewModel else { return true } let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string) switch textField.tag { case TextFieldTags.emailTextField: viewModel.emailAddress = newString case TextFieldTags.passwordTextField: viewModel.password = newString case TextFieldTags.confirmPasswordTextField: viewModel.passwordConfirmation = newString default: break } return true }
pentru a lega controlerul de vizualizare la modelul de vizualizare adecvat (rețineți că acest pas este o cerință a arhitecturii MVVM). Actualizat AppDelegate
codul ar trebui să arate astfel:AppDelegate
Fișierul storyboard și func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { initializeStartingView() return true } fileprivate func initializeStartingView() { if let rootViewController = window?.rootViewController as? RegistrationViewController { let networkService = NetworkServiceImpl() let viewModel = RegisterationViewModel(networkService: networkService) rootViewController.viewModel = viewModel } }
sunt într-adevăr simple, dar sunt adecvate pentru a demonstra modul în care funcționează testarea automatizată a interfeței de utilizare.
Dacă totul este configurat corect, butonul de înregistrare ar trebui să fie dezactivat la pornirea aplicației. Când și numai când, toate câmpurile sunt completate și valide, butonul de înregistrare ar trebui să fie activat.
Odată ce acest lucru este configurat, puteți crea primul test UI.
Testul nostru UI ar trebui să verifice dacă butonul Înregistrare va deveni activ dacă și numai dacă au fost introduse o adresă de e-mail validă, o parolă validă și o confirmare validă a parolei. Iată cum puteți configura acest lucru:
RegistrationViewController
fişier.TestingIOSUITests.swift
metoda și adăugați un testExample()
metodă.testRegistrationButtonEnabled()
metodă de genul că vei scrie ceva acolo.
Puteți înregistra toate instrucțiunile UI utilizând această caracteristică, dar s-ar putea să constatați că scrierea manuală a instrucțiunilor simple va fi mult mai rapidă.
Acesta este un exemplu de instrucțiuni de înregistrare pentru a atinge un câmp de text cu parolă și a introduce o adresă de e-mail „ [e-mail protejat] '
testRegistrationButtonEnabled
let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ')
pentru a testa diferite stări ale aplicației sau elementelor UI.
Instrucțiunile înregistrate nu sunt întotdeauna explicative și pot chiar să facă întreaga metodă de testare puțin greu de citit și de înțeles. Din fericire, puteți introduce manual instrucțiunile UI.
Să creăm următoarele instrucțiuni de interfață manual:
Pentru a face referire la un element de interfață, puteți utiliza un identificator de substituent. Un identificator de substituent poate fi setat în storyboard în panoul Inspector de identitate din Accesibilitate. Setați identificatorul de accesibilitate al câmpului de text al parolei la „passwordTextField”.
Interacțiunea cu interfața de utilizare a parolei poate fi acum scrisă ca:
XCTAsserts
A mai rămas o interacțiune UI: interacțiunea de introducere a parolei de confirmare. De data aceasta, veți face referire la câmpul de text pentru confirmarea parolei în funcție de substituentul său. Accesați storyboard-ul și adăugați substituentul „Confirmare parolă” pentru câmpul text de confirmare a parolei. Acum interacțiunea cu utilizatorul poate fi scrisă astfel:
let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password')
Acum, când aveți toate interacțiunile UI necesare, nu mai rămâne decât să scrieți un simplu let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password')
(la fel cum ați făcut în testarea unitară) pentru a verifica dacă butonul Înregistrare | | + + _ | starea este setată la adevărat. Butonul de înregistrare poate fi menționat folosind titlul său. Afișează pentru a verifica XCTAssert
un buton proprietatea arată astfel:
isEnabled
Întregul test UI ar trebui să arate acum:
isEnabled
Dacă testul este executat, Xcode va porni simulatorul și va lansa aplicația noastră de testare. După lansarea aplicației, instrucțiunile noastre de interacțiune cu interfața de utilizare vor fi rulate unul câte unul, iar la final afirmarea va fi afirmată cu succes.
Pentru a îmbunătăți testul, să testăm, de asemenea, că let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled')
proprietatea butonului de înregistrare este falsă ori de câte ori oricare dintre câmpurile obligatorii nu a fost introdus corect.
Metoda completă de testare ar trebui să arate acum:
func testRegistrationButtonEnabled() { // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ') // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password') // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password') let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled') }
Sfat: Modul preferat de identificare a elementelor de interfață este prin utilizarea identificatorilor de accesibilitate. Dacă se utilizează nume, substituenți sau alte proprietăți care pot fi localizate, elementul nu va fi găsit dacă se folosește o altă limbă, caz în care testul ar eșua.
Exemplul testului UI este foarte simplu, dar demonstrează puterea testării UI automate.
Cel mai bun mod de a descoperi toate posibilitățile (și există multe) ale cadrului de testare UI inclus în Xcode este să începeți să scrieți teste UI în proiectele dvs. Începeți cu povești simple ale utilizatorilor, precum cea afișată, și treceți încet la povești și teste mai complexe.
Din experiența mea, învățarea și încercarea de a scrie teste bune te vor face să te gândești la alte aspecte ale dezvoltării. Te va ajuta să devii o persoană mai bună Dezvoltator iOS cu totul.
Pentru a scrie teste bune, va trebui să învățați cum să vă organizați mai bine codul.
Codul organizat, modular, bine scris este principala cerință pentru testarea cu succes și fără stres a unității și a UI.
În unele cazuri, este chiar imposibil să scrieți teste atunci când codul nu este bine organizat.
Când vă gândiți la structura aplicației și la organizarea codului, veți realiza că, utilizând MVVM, MVP, VIPER sau alte astfel de modele, codul dvs. va fi mai bine structurat, modular și ușor de testat (veți evita, de asemenea, problemele controlerului de vizualizare masivă) .
Când scrieți teste, va trebui, fără îndoială, la un moment dat să creați o clasă batjocorită. Vă va face să vă gândiți și să aflați despre principiul injectării dependenței și practicilor de codificare orientate spre protocol. Cunoașterea și utilizarea acestor principii va crește în mod semnificativ calitatea codului proiectelor viitoare.
Odată ce ați început să scrieți teste, probabil că vă veți observa că vă gândiți mai mult la carcasele de colț și la condițiile de margine în timp ce vă scrieți codul. Acest lucru vă va ajuta să eliminați posibilele erori înainte ca acestea să devină erori. Gândindu-vă la posibile probleme și la rezultatele negative ale metodelor, nu veți testa doar rezultatele pozitive, dar veți începe și să testați rezultatele negative.
După cum puteți vedea, testele unitare pot avea impact asupra diferitelor aspecte de dezvoltare și, scriind teste bune de unitate și de interfață, veți deveni probabil un dezvoltator mai bun și mai fericit (și nu va trebui să petreceți cât mai mult timp pentru remedierea erorilor).
Începeți să scrieți teste automate și, în cele din urmă, veți vedea avantajele testării automate. Când îl veți vedea, veți deveni cel mai puternic avocat al acestuia.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let viewModel = viewModel else { return true } let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string) switch textField.tag { case TextFieldTags.emailTextField: viewModel.emailAddress = newString case TextFieldTags.passwordTextField: viewModel.password = newString case TextFieldTags.confirmPasswordTextField: viewModel.passwordConfirmation = newString default: break } return true }
pentru a lega controlerul de vizualizare la modelul de vizualizare adecvat (rețineți că acest pas este o cerință a arhitecturii MVVM). Actualizat AppDelegate
codul ar trebui să arate astfel:AppDelegate
Fișierul storyboard și func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { initializeStartingView() return true } fileprivate func initializeStartingView() { if let rootViewController = window?.rootViewController as? RegistrationViewController { let networkService = NetworkServiceImpl() let viewModel = RegisterationViewModel(networkService: networkService) rootViewController.viewModel = viewModel } }
sunt într-adevăr simple, dar sunt adecvate pentru a demonstra modul în care funcționează testarea automatizată a interfeței de utilizare.
Dacă totul este configurat corect, butonul de înregistrare ar trebui să fie dezactivat la pornirea aplicației. Când și numai când, toate câmpurile sunt completate și valide, butonul de înregistrare ar trebui să fie activat.
Odată ce acest lucru este configurat, puteți crea primul test UI.
Testul nostru UI ar trebui să verifice dacă butonul Înregistrare va deveni activ dacă și numai dacă au fost introduse o adresă de e-mail validă, o parolă validă și o confirmare validă a parolei. Iată cum puteți configura acest lucru:
RegistrationViewController
fişier.TestingIOSUITests.swift
metoda și adăugați un testExample()
metodă.testRegistrationButtonEnabled()
metodă de genul că vei scrie ceva acolo.
Puteți înregistra toate instrucțiunile UI utilizând această caracteristică, dar s-ar putea să constatați că scrierea manuală a instrucțiunilor simple va fi mult mai rapidă.
Acesta este un exemplu de instrucțiuni de înregistrare pentru a atinge un câmp de text cu parolă și a introduce o adresă de e-mail „ [e-mail protejat] '
testRegistrationButtonEnabled
let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ')
pentru a testa diferite stări ale aplicației sau elementelor UI.
Instrucțiunile înregistrate nu sunt întotdeauna explicative și pot chiar să facă întreaga metodă de testare puțin greu de citit și de înțeles. Din fericire, puteți introduce manual instrucțiunile UI.
Să creăm următoarele instrucțiuni de interfață manual:
Pentru a face referire la un element de interfață, puteți utiliza un identificator de substituent. Un identificator de substituent poate fi setat în storyboard în panoul Inspector de identitate din Accesibilitate. Setați identificatorul de accesibilitate al câmpului de text al parolei la „passwordTextField”.
Interacțiunea cu interfața de utilizare a parolei poate fi acum scrisă ca:
XCTAsserts
A mai rămas o interacțiune UI: interacțiunea de introducere a parolei de confirmare. De data aceasta, veți face referire la câmpul de text pentru confirmarea parolei în funcție de substituentul său. Accesați storyboard-ul și adăugați substituentul „Confirmare parolă” pentru câmpul text de confirmare a parolei. Acum interacțiunea cu utilizatorul poate fi scrisă astfel:
let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password')
Acum, când aveți toate interacțiunile UI necesare, nu mai rămâne decât să scrieți un simplu let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password')
(la fel cum ați făcut în testarea unitară) pentru a verifica dacă butonul Înregistrare | | + + _ | starea este setată la adevărat. Butonul de înregistrare poate fi menționat folosind titlul său. Afișează pentru a verifica XCTAssert
un buton proprietatea arată astfel:
isEnabled
Întregul test UI ar trebui să arate acum:
isEnabled
Dacă testul este executat, Xcode va porni simulatorul și va lansa aplicația noastră de testare. După lansarea aplicației, instrucțiunile noastre de interacțiune cu interfața de utilizare vor fi rulate unul câte unul, iar la final afirmarea va fi afirmată cu succes.
Pentru a îmbunătăți testul, să testăm, de asemenea, că let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled')
proprietatea butonului de înregistrare este falsă ori de câte ori oricare dintre câmpurile obligatorii nu a fost introdus corect.
Metoda completă de testare ar trebui să arate acum:
func testRegistrationButtonEnabled() { // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ') // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password') // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password') let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled') }
Sfat: Modul preferat de identificare a elementelor de interfață este prin utilizarea identificatorilor de accesibilitate. Dacă se utilizează nume, substituenți sau alte proprietăți care pot fi localizate, elementul nu va fi găsit dacă se folosește o altă limbă, caz în care testul ar eșua.
Exemplul testului UI este foarte simplu, dar demonstrează puterea testării UI automate.
Cel mai bun mod de a descoperi toate posibilitățile (și există multe) ale cadrului de testare UI inclus în Xcode este să începeți să scrieți teste UI în proiectele dvs. Începeți cu povești simple ale utilizatorilor, precum cea afișată, și treceți încet la povești și teste mai complexe.
Din experiența mea, învățarea și încercarea de a scrie teste bune te vor face să te gândești la alte aspecte ale dezvoltării. Te va ajuta să devii o persoană mai bună Dezvoltator iOS cu totul.
Pentru a scrie teste bune, va trebui să învățați cum să vă organizați mai bine codul.
Codul organizat, modular, bine scris este principala cerință pentru testarea cu succes și fără stres a unității și a UI.
În unele cazuri, este chiar imposibil să scrieți teste atunci când codul nu este bine organizat.
Când vă gândiți la structura aplicației și la organizarea codului, veți realiza că, utilizând MVVM, MVP, VIPER sau alte astfel de modele, codul dvs. va fi mai bine structurat, modular și ușor de testat (veți evita, de asemenea, problemele controlerului de vizualizare masivă) .
Când scrieți teste, va trebui, fără îndoială, la un moment dat să creați o clasă batjocorită. Vă va face să vă gândiți și să aflați despre principiul injectării dependenței și practicilor de codificare orientate spre protocol. Cunoașterea și utilizarea acestor principii va crește în mod semnificativ calitatea codului proiectelor viitoare.
Odată ce ați început să scrieți teste, probabil că vă veți observa că vă gândiți mai mult la carcasele de colț și la condițiile de margine în timp ce vă scrieți codul. Acest lucru vă va ajuta să eliminați posibilele erori înainte ca acestea să devină erori. Gândindu-vă la posibile probleme și la rezultatele negative ale metodelor, nu veți testa doar rezultatele pozitive, dar veți începe și să testați rezultatele negative.
După cum puteți vedea, testele unitare pot avea impact asupra diferitelor aspecte de dezvoltare și, scriind teste bune de unitate și de interfață, veți deveni probabil un dezvoltator mai bun și mai fericit (și nu va trebui să petreceți cât mai mult timp pentru remedierea erorilor).
Începeți să scrieți teste automate și, în cele din urmă, veți vedea avantajele testării automate. Când îl veți vedea, veți deveni cel mai puternic avocat al acestuia.