Matteo Manferdini

Mulți dezvoltatori consideră că testarea unitară este confuză. Acest lucru este înrăutățit de tehnicile avansate de care aveți nevoie pentru a testa clasele sau codul asincron, cum ar fi cel pentru cererile de rețea.

În realitate, la baza testării unitare se află concepte fundamentale simple. Odată ce le înțelegeți pe acestea, testarea unitară devine brusc mai ușor de abordat.

Contenit

  • Testarea unitară explicată în mod simplu, termeni ușor de înțeles
  • Testarea unitară vă permite să verificați cazuri limită greu de atins și să preveniți erorile
  • Scrierea testelor unitare în Xcode
  • O arhitectură bună face codul mai ușor de testat
  • Tratarea tipurilor de valori ca funcții pure pentru a simplifica testarea unitară
  • .
  • Verificarea codului dvs. utilizând funcțiile de aserțiuni din cadrul XCTest
  • Măsurarea acoperirii de cod a unui test în Xcode
  • De ce este lipsit de sens să urmăriți o acoperire specifică a codului și ce ar trebui să faceți în schimb

Testarea unitară explicată simplu, termeni ușor de înțeles

Când scrieți o aplicație, treceți printr-un ciclu de scriere a codului, de rulare a acestuia și de verificare dacă funcționează așa cum s-a dorit. Dar, pe măsură ce codul dvs. crește, devine mai greu să testați manual întreaga aplicație.

Testarea automatizată poate efectua aceste teste pentru dumneavoastră. Deci, practic vorbind, testarea unitară este un cod pentru a vă asigura că codul aplicației dvs. este corect.

Un alt motiv pentru care testarea unitară poate fi greu de înțeles este faptul că multe concepte o înconjoară: testarea în Xcode, arhitectura aplicației, dublurile de testare, injectarea dependențelor și așa mai departe.

Dar, în esența sa, ideea de testare unitară este destul de simplă. Puteți scrie teste unitare simple în Swift simplu. Nu aveți nevoie de nicio caracteristică specială Xcode sau tehnică sofisticată și le puteți chiar rula în interiorul unui Swift Playground.

Să ne uităm la un exemplu. Funcția de mai jos calculează factorialul unui număr întreg, care este produsul tuturor numerelor întregi pozitive mai mici sau egale cu cele de intrare.

1
2
3
4
5
6
7

func factorial(de număr: Int) -> Int {
var result = 1
for factor in 1…număr {
rezultat = rezultat * factor
} }
return result
}

Pentru a testa manual această funcție, îi veți da câteva numere și veți verifica dacă rezultatul este cel așteptat. Astfel, de exemplu, ați încerca-o pe patru și ați verifica dacă rezultatul este 24.

Puteți automatiza acest lucru, scriind o funcție Swift care să facă testul în locul dvs. De exemplu:

1
2
3
4
5
5

func testFactorial() {
if factorial(of: 4) != 24 {
print(„Factoriala lui 3 este greșită”)
}
}

Acesta este un cod Swift simplu pe care toată lumea îl poate înțelege. Tot ce face este să verifice ieșirea funcției și să tipărească un mesaj dacă nu este corectă.

Asta este testarea unitară pe scurt. Tot restul sunt clopote și fluiere adăugate de Xcode pentru a face testarea mai simplă.

O cel puțin, așa ar fi dacă codul din aplicațiile noastre ar fi la fel de simplu ca funcția factorială pe care tocmai am scris-o. De aceea aveți nevoie de suportul Xcode și de tehnici avansate cum ar fi testele duble.

Cu toate acestea, această idee este utilă atât pentru testele simple, cât și pentru cele complexe.

Testarea unitară vă permite să verificați cazuri limită greu de atins și să preveniți erorile

Puteți obține mai multe beneficii din testarea unitară.

Cel mai evident este că nu trebuie să vă testați continuu codul manual. Poate fi plictisitor să rulați aplicația dvs. și să ajungeți într-o anumită locație cu funcționalitatea pe care doriți să o testați.

Dar asta nu este tot. Testele unitare vă permit, de asemenea, să testați cazuri limită care ar putea fi greu de creat într-o aplicație care rulează. Iar cazurile limită sunt cele în care trăiesc cele mai multe bug-uri.

De exemplu, ce se întâmplă dacă introducem zero sau un număr negativ în funcția noastră factorial(of:) de mai sus?

1
2
3
4
5
5

func TestFactorialOfZero() {
if factorial(of: 0) != 1 {
print(„factorialul lui 0 este greșit”)
}
}

Codul nostru se rupe:

Într-o aplicație reală, acest cod ar provoca un accident. Zero este în afara intervalului în bucla for.

Dar codul nostru nu eșuează din cauza unei limitări a intervalelor Swift. El eșuează pentru că am uitat să luăm în considerare numerele întregi non-pozitive în codul nostru. Prin definiție, factorialul unui număr întreg negativ nu există, în timp ce factorialul lui 0 este 1.

1
2
3
4
5
6
7
8
9
10
11
12
13

func factorial(de număr: Int) -> Int? {
if (number < 0) {
return nil
} }
if (number == 0) {
return 1
} }
var rezultat = 1
for factor in 1…număr {
rezultat = rezultat * factor
}
return result
}

Acum putem rula din nou testele noastre și chiar să adăugăm o verificare pentru numere negative.

1
2
3
4
5

func testFactorialOfNegativeInteger() {
if factorial(of: -1) != nil {
print(„Factorialul lui -1 este greșit”)
}
}

Aici vedeți ultimul și, în opinia mea, cel mai important beneficiu al testării unitare. Testele pe care le-am scris anterior se asigură că, pe măsură ce actualizăm funcția noastră factorial(of:), nu stricăm codul său existent.

Acest lucru este crucial în aplicațiile complexe, unde trebuie să adăugați, să actualizați și să ștergeți cod în mod continuu. Testarea unitară vă oferă încrederea că modificările dvs. nu au stricat codul care funcționa bine.

Scrierea testelor unitare în Xcode

Cu o înțelegere a testelor unitare, putem acum să ne uităm la caracteristicile de testare din Xcode.

Când creați un nou proiect Xcode, aveți șansa de a adăuga teste unitare imediat. Voi folosi, ca exemplu, o aplicație pentru urmărirea caloriilor. Puteți găsi proiectul Xcode complet aici.

Când bifați această opțiune, proiectul șablon va include o țintă de testare deja configurată. Puteți adăuga oricând una mai târziu, dar eu de obicei bifez opțiunea în mod implicit. Chiar dacă nu am de gând să scriu teste imediat, totul va fi configurat atunci când voi decide să o fac.

Obiectivul de testare începe deja cu ceva cod de șablon pentru testele noastre.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
26

import XCTest
@testable import Calories
clasa CaloriesTests: XCTestCase {
override func setUpWithError() throws {
// Puneți codul de configurare aici. Această metodă este apelată înainte de invocarea fiecărei metode de testare din clasă.
}
override func tearDownWithError() throws {
// Puneți codul de dezactivare aici. Această metodă este apelată după invocarea fiecărei metode de testare din clasă.
}
func testExample() throws {
// Acesta este un exemplu de caz de test funcțional.
// Utilizați XCTAssert și funcțiile aferente pentru a verifica dacă testele dvs. produc rezultatele corecte.
}
func testPerformanceExample() throws {
// Acesta este un exemplu de caz de test de performanță.
self.measure {
// Puneți aici codul al cărui timp doriți să îl măsurați.
}
}
}

  • Linia @testable import Calories vă oferă acces la toate tipurile din ținta principală a proiectului dumneavoastră. Codul unui modul Swift nu este accesibil din exterior decât dacă toate tipurile și metodele sunt declarate în mod explicit ca fiind publice. Cuvântul cheie @testable vă permite să ocoliți această limitare și să accesați chiar și codul privat.
  • Clasa CaloriesTests reprezintă un caz de testare, adică o grupare de teste conexe. O clasă de caz de test trebuie să descindă din XCTestCase.
  • Metoda setUpWithError() vă permite să configurați un mediu standard pentru toate testele dintr-un caz de test. Dacă aveți nevoie să faceți curățenie după teste, utilizați metoda tearDownWithError(). Aceste metode se execută înainte și după fiecare test dintr-un caz de testare. Nu vom avea nevoie de ele, așa că le puteți șterge.
  • Metoda testExample() este un test ca cele pe care le-am scris pentru exemplul nostru factorial. Puteți numi metodele de test cum doriți, dar acestea trebuie să înceapă cu prefixul test pentru ca Xcode să le recunoască.
  • Și, în final, testulExempluPerformanceExample() vă arată cum să măsurați performanța codului dumneavoastră. Testele de performanță sunt mai puțin standard decât testele unitare și le folosiți numai pentru codul crucial care trebuie să fie rapid. Nu vom folosi nici această metodă.

Puteți găsi toate cazurile de testare și testele relative listate în navigatorul Test din Xcode.

Puteți adăuga noi cazuri de testare la proiectul dvs. prin simpla creare de noi teste .swift cu cod ca cel de mai sus, dar este mai ușor să folosiți meniul de adăugare din partea de jos a navigatorului Test.

O arhitectură bună face ca codul dvs. să fie mai ușor de testat

Într-un proiect real, de obicei nu aveți funcții libere ca în exemplul nostru factorial. În schimb, aveți structuri, enumerări și clase care reprezintă diferitele părți ale aplicației dumneavoastră.

Este esențial să vă organizați codul în conformitate cu un model de proiectare precum MVC sau MVVM. Faptul de a avea un cod bine arhitecturat nu numai că vă face codul bine organizat și mai ușor de întreținut. De asemenea, facilitează testarea unitară.

Acest lucru ne ajută, de asemenea, să răspundem la o întrebare comună: ce cod ar trebui să testați mai întâi?

Răspunsul este: începeți de la tipurile de model.

Există mai multe motive pentru acest lucru:

  • Dacă urmați modelul MVC sau unul dintre derivatele sale, tipurile de model vor conține logica de afaceri a domeniului. Întreaga dvs. aplicație depinde de corectitudinea unui astfel de cod, astfel încât chiar și câteva teste au un impact semnificativ.
  • Tipurile de model sunt adesea structuri, care sunt tipuri de valori. Acestea sunt mult mai ușor de testat decât tipurile de referință (vom vedea de ce într-un moment).
  • Tipurile de referință, adică clasele, au dependențe și provoacă efecte secundare. Pentru a scrie teste unitare pentru acestea, aveți nevoie de dubluri de testare (manechine, stubs, fakes, spioni și mocks) și folosiți tehnici sofisticate.
  • Controlerele (în termeni MVC) sau modelele de vizualizare (în termeni MVVM) sunt adesea conectate la resurse cum ar fi stocarea pe disc, o bază de date, rețeaua și senzorii dispozitivului. De asemenea, acestea ar putea rula cod paralel. Acestea îngreunează testarea unitară.
  • Codul de vizualizare este cel care se modifică cel mai frecvent, producând teste fragile care se rup ușor. Și nimănui nu-i place să repare testele rupte.

Tratarea tipurilor de valori ca funcții pure pentru a simplifica testarea unitară

Acum, să adăugăm câteva tipuri de modele în aplicația noastră.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

struct FoodItem {
let name: String
let caloriesFor100Grams: Int
let grams: Int
}
struct Meal {
private(set) var items: =
var calories: Int {
var calories = 0
for item in items {
calories += (item.caloriesFor100Grams / 100) * item.grams
} }
return calories
} }
mutating func add(_ item: FoodItem) {
items.append(item)
}
}

Structura Meal conține o logică simplă de adăugare a elementelor alimentare și de calcul al caloriilor totale ale alimentelor pe care le conține.

Testarea unităților are un astfel de nume deoarece ar trebui să vă testați codul în unități separate, eliminând toate dependențele (testarea împreună a dependențelor se numește în schimb testare de integrare). Fiecare unitate este apoi tratată ca o cutie neagră. Îi furnizați niște intrări și îi testați ieșirea pentru a verifica dacă este ceea ce vă așteptați.

Acesta a fost ușor în cazul exemplului nostru factorial pentru că a fost o funcție pură, adică o funcție în care ieșirea depinde doar de intrare și care nu creează niciun efect secundar.

Dar nu pare să fie cazul pentru tipul nostru Meal, care conține stare.

Valoarea returnată de proprietatea calculată calorii nu are nicio intrare. Ea depinde doar de conținutul tabloului de elemente. Metoda add(_:), în schimb, nu returnează nicio valoare și doar modifică starea internă a unei mese.

Dar structurile sunt tipuri de valoare și le putem considera funcții pure. Deoarece nu au dependențe, puteți considera starea inițială a unei structuri ca fiind intrarea unei metode și starea acesteia după apelarea metodei ca fiind ieșirea.

(Acesta este unul dintre motivele pentru care ar trebui să vă abțineți de la a pune tipuri de referință în interiorul tipurilor de valoare).

Verificarea codului dvs. folosind funcțiile de aserțiuni din cadrul XCTest

Acum avem un cod de testat și un loc în proiectul nostru Xcode unde să scriem testele.

Ne lipsește un ultim ingredient: o modalitate de a exprima dacă codul nostru este corect sau nu.

În exemplul de la începutul acestui articol, am folosit simple instrucțiuni print. Acest lucru nu este practic pentru testarea unitară reală. Nu doriți să pierdeți timp căutând prin jurnale pentru a identifica testele care au eșuat. Avem nevoie de o modalitate care să indice direct testele eșuate.

În cadrul XCTest, găsiți o serie de funcții de afirmație care fac ca Xcode să indice testele care nu trec.

Cu acestea, putem scrie primul nostru test.

Cum am menționat mai sus, testele unitare sunt utile pentru a verifica cazurile limită, așa că să începem să ne asigurăm că o masă goală nu are calorii.

1
2
3
4
5
5
6

clasa CaloriesTests: XCTestCase {
func testEmptyMeal() throws {
let meal = Meal()
XCTAssertEqual(meal.calories, 0, „O masă goală ar trebui să aibă 0 calorii”)
} }
}

XCTest oferă o funcție XCTAssert generică care vă permite să afirmați orice condiție. Ați putea să o folosiți pentru orice test pe care îl scrieți, dar este mai bine să folosiți funcții de aserțiune mai specializate atunci când este posibil, cum ar fi XCTAssertEqual (mai sus), XCTAssertNil și altele. Acestea produc erori mai bune în Xcode decât XCTAssert.

Puteți rula un singur test făcând clic pe diamantul de lângă numele său în jgheabul editorului de cod.

Când un test trece, acesta devine verde, în timp ce testele care eșuează sunt marcate cu roșu. Puteți schimba cifra 0 din test cu orice alt număr pentru a vedea cum arată un test eșuat.

Puteți rula toate testele dintr-un caz de test făcând clic pe diamantul de lângă declarația clasei. De asemenea, puteți utiliza navigatorul Test pe care l-am văzut mai sus pentru a rula teste individuale sau cazuri de testare.

Deseori doriți să rulați toate testele din proiect deodată, lucru pe care îl puteți face rapid apăsând cmd+U pe tastatură sau selectând opțiunea Test din meniul Produs din Xcode.

Măsurarea acoperirii codului unui test în Xcode

Întrebarea următoare, cea mai frecventă despre testarea unitară este: cât de mult din codul dvs. ar trebui să testați?

De obicei, acest lucru se măsoară prin acoperirea codului, adică, procentul de cod acoperit de testele dumneavoastră. Așadar, înainte de a răspunde la această întrebare, haideți să vedem cum puteți măsura acoperirea de cod a testelor dvs. în Xcode.

În primul rând, trebuie să lăsați Xcode să adune acoperirea pentru ținta testului dvs. Faceți clic pe butonul cu numele proiectului din controlul segmentat din bara de instrumente Xcode (lângă butonul de oprire). Apoi, selectați Edit scheme…. în meniul pop-up.

Acolo, selectați Test în contur, apoi mergeți la fila Options. Și, în cele din urmă, selectați Gather coverage for. Puteți lăsa opțiunea sa la toate țintele.

Acum puteți relua testele, ceea ce va permite Xcode să adune datele de acoperire. Veți găsi rezultatul în navigatorul Report (Raport), selectând elementul Code coverage (Acoperire cod) din contur.

Aici, puteți verifica procentele de cod acoperite de testele dvs. pentru întregul proiect și pentru fiecare fișier.

Aceste numere nu sunt atât de semnificative pentru exemplul nostru, deoarece nu am scris aproape deloc cod. Totuși, putem vedea că metoda add(_:) a tipului Meal are o acoperire de 0%, ceea ce înseamnă că nu am testat-o încă.

Proprietatea calories computed are, în schimb, o acoperire de 85,7%, ceea ce înseamnă că există unele căi de execuție în codul nostru pe care testul nostru nu le-a declanșat.

În exemplul nostru simplu, este ușor de înțeles despre ce cale este vorba. Am testat doar caloriile unei mese goale, deci codul din bucla for nu a fost executat.

În metode mai sofisticate, însă, s-ar putea să nu fie la fel de simplu.

Pentru aceasta, puteți scoate la iveală banda de acoperire a codului în editorul Xcode. O puteți găsi în meniul Adjust Editor Options (Reglați opțiunile editorului) din colțul din dreapta sus.

Aceasta va scoate la iveală o bandă care vă arată acoperirea pentru fiecare linie de cod.

Numerele vă spun de câte ori a fost executată fiecare linie în timpul testării. Cu roșu, puteți vedea care linii nu au fost executate deloc (complet) sau au fost executate doar parțial (dungat).

De ce este lipsit de sens să urmăriți o acoperire specifică a codului și ce ar trebui să faceți în schimb

Atunci, ce procent este un procent bun de acoperire a codului?

Opinioanele sunt foarte diferite. Unii dezvoltatori consideră că 20% este suficient. Alții merg pe cifre mai mari, cum ar fi 70% sau 80%. Există chiar și dezvoltatori care cred că doar 100% este acceptabil.

În opinia mea, acoperirea codului este o măsurătoare fără sens. O puteți folosi pentru a vă informa deciziile privind testarea, dar nu ar trebui să o tratați ca pe un obiectiv pe care trebuie să îl atingeți.

Pentru a vedea de ce, haideți să scriem un alt test pentru a acoperi codul pe care nu l-am testat încă.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
18
19

clasa CaloriesTests: XCTestCase {
let banana = FoodItem(name: „Banana”, caloriesFor100Grams: 89, grams: 118)
let steak = FoodItem(name: „Steak”, caloriesFor100Grams: 271, grams: 225)
let goatCheese = FoodItem(name: „Goat Cheese”, caloriesFor100Grams: 364, grams: 28)
func testEmptyMeal() throws {
let meal = Meal()
XCTAssertEqual(meal.calories, 0, „An empty meal should have 0 calories”)
}
func testCalories() {
var meal = Meal()
meal.add(banana)
meal.add(steak)
meal.add(goatCheese)
XCTAssertEqual(meal.items.count, 3)
XCTAssertEqual(meal.calories, 534)
}
}

Dacă rulați din nou toate testele, veți obține o acoperire de 100% pentru fișierul Model.swift. Arată bine, nu-i așa?

Acum, mergeți și eliminați metoda testEmptyMeal(). Dacă rulați singuri testele rămase, veți vedea că acoperirea pentru tipurile noastre Meal este încă de 100%.

Acest lucru vă arată deja că numărul de 100% vă poate da un fals sentiment de siguranță. Știți că acum nu testăm toate cazurile limită pentru proprietatea calculată calorii. Și totuși, acoperirea testului nostru nu reflectă acest lucru.

Am adăuga că o acoperire de 100% nu este doar înșelătoare, ci chiar dăunătoare. Unele coduri din proiectul dvs. sunt predispuse la schimbări constante, în special cele mai noi coduri și mai ales în vizualizări.

Coperirea de 100% din acesta înseamnă că nu veți avea decât să produceți teste fragile care se strică continuu și pe care trebuie să le reparați. Aceste teste nu detectează bug-uri. Ele se sparg pentru că funcționalitatea codului dumneavoastră se schimbă.

Și aici ajungem la punctul despre care nimeni nu vorbește.

Scrierea testelor nu este amuzantă, în ciuda a ceea ce susțin unii dezvoltatori. Corectarea testelor este și mai puțin amuzantă. Faceți-o prea mult și veți urî testarea unitară și o veți da la o parte cu totul.

Psihologia este la fel de esențială pentru dezvoltarea de software ca și cele mai bune practici. Noi nu suntem mașini.

Atunci, ce număr ar trebui să urmăriți? Asta necesită o discuție mai extinsă, dar aici vă voi oferi un rezumat al opiniei mele.

Scrieți teste utile, începând cu cel mai vechi cod din proiectul dumneavoastră care nu se va schimba. Folosiți acoperirea codului doar pentru a identifica ce cod nu este încă testat, dar nu o folosiți ca obiectiv sau pentru a decide când să scrieți mai multe teste. O acoperire de 20% este mai bună decât 80% dacă testele dvs. sunt semnificative și testați codul potrivit.

După cum am menționat mai sus, tipurile de modele conțin cel mai critic cod din proiectul dvs. care, de asemenea, tinde să se schimbe cel mai puțin. Așadar, începeți de acolo.

De obicei nu mă deranjez să testez fiecare cale de execuție. De exemplu, într-un proiect real, nu aș scrie metoda testEmptyMeal() pe care v-am arătat-o mai sus. Da, este crucial să testați cazurile limită, dar nici nu sunt toate importante.

De obicei aș scrie doar testul testCalories(). Asta îmi spune că codul meu funcționează și mă va avertiza dacă, mai târziu, voi face o greșeală atunci când voi modifica această metodă.

Sigur, nu acoperă toate căile, dar acest cod este atât de simplu încât asta este doar o pierdere de timp. Să-ți petreci timpul scriind cod real pentru caracteristici care ajută utilizatorii tăi este mai important decât să testezi fiecare cale de execuție.

Știu că voi primi unele critici din partea unor dezvoltatori pentru această opinie, dar nu-mi pasă. Acoperirea testelor este una dintre acele dezbateri care nu se va termina niciodată.

Codul pe care îl scrieți este de obicei în mare parte corect. Nu este nevoie să fii paranoic. A avea o pletoră de teste care se strică de fiecare dată când schimbați ceva este o datorie, nu un avantaj.

Timpurile și voința dumneavoastră sunt limitate. Cheltuiți-le pe sarcini mai importante.

Arhitectura aplicațiilor SwiftUI cu MVC și MVVM

Este ușor să faci o aplicație aruncând niște cod împreună. Dar fără cele mai bune practici și o arhitectură robustă, în curând veți ajunge la un cod spaghete imposibil de gestionat. În acest ghid vă voi arăta cum să structurați corect aplicațiile SwiftUI.

Obțineți cartea gratuită acum

.

Lasă un răspuns

Adresa ta de email nu va fi publicată.