Scrierea codului testabil
Publicat: 2022-11-03Testarea unitară este un instrument esențial în cutia de instrumente a oricărui dezvoltator de software. Scrierea testelor unitare este relativ ușoară atunci când aveți de-a face cu o bază de cod care urmează cele mai bune practici, modele și principii de proiectare a software-ului. Adevărata problemă apare atunci când încercați să testați un cod prost proiectat, netestabil.
Acest blog va discuta despre cum să scrieți mai mult cod testabil în unități și ce modele și practici proaste trebuie evitate pentru a îmbunătăți testabilitatea.
Cod testabil și netestabil
Când lucrăm la aplicații la scară largă care trebuie să fie întreținute pe termen lung, trebuie să ne bazăm pe teste automate pentru a menține calitatea generală a sistemului ridicată. În comparație cu testele de integrare, în care testați mai multe unități în ansamblu, testele unitare au avantajul de a fi rapide și stabile. Rapid pentru că instanțiăm, în mod ideal, doar clasa supusă testului și stabil pentru că de obicei batem joc de dependențele externe, de exemplu, baza de date sau conexiunea la rețea.
Dacă nu sunteți familiarizat cu diferența exactă dintre testele unitare și cele de integrare, puteți citi mai multe despre acest subiect în blogul nostru Introducere în testare.
Codul testabil poate fi izolat de restul bazei de cod. Cu alte cuvinte, cele mai mici unități pot fi testate independent. Codul netestabil este scris în așa fel încât este greu, sau chiar imposibil, să scrieți un test unitar bun pentru el.
Să trecem în revistă câteva anti-modele și practici proaste pe care ar trebui să le evităm atunci când scriem cod testabil.
Exemplele sunt scrise în Java, dar convențiile de codare menționate aici se aplică oricărui limbaj de programare orientat pe obiecte și cadru de testare. Vom folosi assertj și JUnit5 pentru exemple în această postare de blog.
Injecție de dependență
Injecția de dependență este unul dintre cele mai importante modele de proiectare pentru realizarea izolării testului. Injecția de dependență este un model de proiectare în care un obiect primește alte obiecte (dependențe) prin intermediul parametrilor constructorului sau al seterilor, în loc să fie nevoit să le construiască singur.
Cu injecția de dependență, putem izola cu ușurință clasa testată prin batjocură de dependențele unui obiect.
Să ne uităm la un exemplu fără injecție de dependență:

Deoarece dependența de motor este construită în constructorul clasei Car, puteți spune că clasele Car și Engine sunt strâns cuplate. Sunt foarte dependenti unul de celălalt – schimbarea unuia ar necesita o schimbare a celuilalt.
Din punct de vedere al testării, nu puteți testa clasa Car în mod izolat, deoarece exemplul de mai sus nu poate înlocui implementarea concretă a Motorului cu un test dublu.
Cu toate acestea, putem obține izolarea prin utilizarea injecției de dependență și a polimorfismului:

Acum putem construi mai multe implementări de motoare și, prin urmare, mașini cu motoare diferite:

Testarea izolat este acum posibilă, deoarece putem crea o implementare batjocoritoare a abstracției Engine și o transmitem clasei noastre Car:

Când aveți de-a face cu obiecte care necesită alte obiecte (dependențe), ar trebui să le furnizați prin intermediul parametrilor constructorului (injecție de dependență), în mod ideal ascunși în spatele unei abstracțiuni.
Urmând acest model, codul dvs. devine mai lizibil și mai adaptabil la schimbarea în timp. De asemenea, ar trebui să evitați să faceți munca efectivă în constructori - orice altceva decât atribuirea de câmp este muncă reală. Cuvântul cheie „nou” din constructori este întotdeauna un semn de avertizare privind codul netestabil.
Un lucru de remarcat aici este că, în unele situații, cuplarea strânsă (de exemplu, cuvinte cheie noi în constructori, clase interioare pentru izolarea logică, mapatori de obiecte) nu este o practică proastă - clase care nu ar avea sens ca o clasă „autonomă”.
Stat global
Partajarea unei stări globale poate produce adesea teste neconforme (uneori trec, alteori eșuează), mai ales în mediile cu mai multe fire.
Imaginați-vă un scenariu în care mai multe obiecte testate împărtășesc aceeași stare globală – dacă o metodă dintr-unul dintre obiecte declanșează un efect secundar care modifică valoarea stării globale partajate, ieșirea dintr-o metodă din celălalt obiect devine imprevizibilă. Evitați utilizarea metodelor statice impure, deoarece acestea modifică starea globală într-un fel sau sunt redirecționate către o stare globală.
Să ne uităm la această metodă statică impură:

În esență, această metodă citește data și ora curentă a sistemului și returnează un rezultat bazat pe acea valoare. Ar fi foarte dificil să scrieți un test unitar adecvat bazat pe stare pentru această metodă, deoarece apelul static LocalDateTime.now() va produce rezultate diferite în timpul executării testelor noastre. Scrierea de teste pentru această metodă este imposibilă fără modificarea datei și orei sistemului.
Pentru a remedia acest lucru, vom trece metoda date-ora la timeOfDay ca argument:


Metoda statică timeOfDay este acum pură – aceleași intrări produc întotdeauna aceleași rezultate. Acum putem trece cu ușurință obiecte dateTime izolate ca argumente în testele noastre:

Legea lui Demeter
Legea lui Demeter, sau principiul cunoașterii minime, afirmă că obiectele ar trebui să știe numai despre obiecte strâns legate de primul obiect. Cu alte cuvinte, un obiect ar trebui să aibă acces numai la obiectele de care are nevoie. De exemplu, avem o metodă care acceptă un obiect context ca argument:

Această metodă încalcă Legea lui Demeter, deoarece trebuie să parcurgă un grafic obiect pentru a obține informațiile necesare pentru a-și face treaba. Transmiterea de informații inutile în clase și metode dăunează testabilității.
Imaginați-vă un obiect BillingContext uriaș care conține referințe la alte obiecte:

După cum putem vedea, testul nostru este plin de informații neesențiale. Testele care creează grafice de obiecte complexe sunt greu de citit și introduc o complexitate inutilă.
Să reparăm exemplul nostru anterior:

Ar trebui să treceți întotdeauna dependențe directe în clase și metode. Cu toate acestea, trecerea multor argumente în metode nu este, de asemenea, o practică bună - în mod ideal, ar trebui să treceți două argumente la maximum sau să includeți argumente apropiate în obiecte de date.
Dumnezeu obiectează
Obiectul lui Dumnezeu este un obiect care face referire la multe alte obiecte distincte, are mai mult de o responsabilitate și are mai multe motive pentru a se schimba. Dacă este dificil să rezumați ceea ce face clasa sau dacă rezumatul include cuvântul „și”, clasa are probabil mai mult de o responsabilitate.
Obiectele lui Dumnezeu sunt greu de testat, deoarece avem de-a face cu dependențe multiple care nu au legătură, amestecând diferite niveluri de abstracție și preocupări și produc o mulțime de efecte secundare. În consecință, este dificil să atingem starea dorită pentru cazurile noastre de testare.
De exemplu:

UserService are mai mult de o responsabilitate – înregistrarea de noi utilizatori și trimiterea de e-mailuri. În timpul testării înregistrării utilizatorilor, trebuie să ne ocupăm de serviciul de e-mail și invers:

Imaginați-vă un UserService cu mai mult de două dependențe fără legătură. Aceste dependențe au propriile lor dependențe și așa mai departe. Am ajunge la un test care este ilizibil, plin de informații fără legătură și foarte greu de înțeles. Prin urmare, fiecare clasă ar trebui să aibă o singură responsabilitate și un motiv pentru a se schimba. O clasă care are un singur motiv de schimbare este unul dintre cele cinci principii de proiectare software numite principiul responsabilității unice.
Puteți citi mai multe despre principiile SOLID aici.
Concluzie
O bază de cod care urmează cele mai bune practici de proiectare software face scrierea testelor unitare mult mai ușor de gestionat.
Pe de altă parte, poate fi foarte dificil, sau uneori chiar imposibil, să scrieți un test unitar de lucru pentru o bază de cod folosind anti-modelele menționate. Scrierea unui cod bun testabil necesită multă practică, disciplină și efort suplimentar. Cel mai semnificativ avantaj al codului testabil este ușurința de testare și capacitatea de a înțelege, menține și extinde acel cod.
Sperăm că acest blog vă ajută să scrieți cod testabil.