Imaginați-vă următoarea situație: o echipă a dezvoltat timp de 6 luni un produs grozav, care se vinde imediat. Utilizatorii își arată pasiunea pentru produs cerând mereu funcționalități noi. Dacă echipa nu livrează noile funcționalități destul de repede, fericirea lor va scădea, poate chiar vor decide să migreze la concurență. Echipa trebuie să livreze rapid.
Din păcate, validarea completă a produsului de către echipa de testare poate dura săptămâni, dacă nu luni. Și asta fără a lua în calcul timpul necesar rezolvării bug-urilor găsite de testeri. Nu se poate așadar ca echipa să livreze la timp dacă produsul este complet testat. Ce e de făcut?
Alternativele cele mai comune sunt:
Toate soluțiile de mai sus înseamnă scăderea temporară a grijii acordate produsului, pentru a asigura livrarea cât mai rapidă. Din păcate pentru echipa din scenariul descris mai sus, se asumă riscuri care pot avea impact major asupra afacerii. Riscul major este scăparea din vedere a unor zone din aplicație și livrarea cu bug-uri. Acest risc poate duce la:
Echipa ar putea valida întreaga aplicație în ore și nu în săptămâni sau luni? Dacă fiecare programator ar putea afla la fiecare mică modificare în cod, în câteva minute, că nu a stricat nimic (cu o probabilitate de 80+%)
Articolul despre Software Craftsmanship, publicat în numărul 11 al Today Software Magazine, menționează ideea principală de la care a pornit mișcarea: un software craftsman poate livra calitate sub presiune. În această situație, un software craftsman ar trebui să livreze funcționalități noi în timpul alocat cu cât mai puține bug-uri. Cât de puține? Mai puțin de 10 per release.
Acest număr este conservator, pentru că metodele agile (inclusiv Scrum) au cerut de la început ca la finalul fiecărui sprint de 2-3 săptămâni, echipa să livreze software cu 0 bug-uri cunoscute, după ce aplicația a fost validată de testeri.
Dar orice aplicație are bug-uri!
Denumirea de bug este un eufemism pentru greșeală. Greșelile sunt normale în orice activitate umană, chiar și atunci când dezvoltăm software.
Întrebarea este cum poate fi redus numărul de greșeli și impactul lor. Unit testing este o unealtă care poate ajuta, dar nu este singura. Alte unelte care pot ajuta sunt: code review, pair programming, reducerea numărului de linii de cod, design by contract.
Testarea unitară se referă la scrierea unor bucăți de cod, denumite cod de testare, care validează codul de producție. Testarea majorității aplicației devine așadar automată.
Istorie: programatorii cred adesea în mod eronat că unit testing este o practică nouă. În realitate, era folosită chiar de pe vremea calculatoarelor mainframe cu cartele perforate. Pe vremea aceea, debugging-ul era foarte dificil din cauză că implica citirea unor foi lungi de zeci de metri imprimate cu rezultatul programului și informații despre execuție. Testele automate care rulau în același timp cu programul dădeau informații mult mai bogate legate de sursa greșelilor.
Ce facem cu testerii? O temere întâlnită adesea este că testerii își vor pierde locul de muncă o dată cu introducerea testării automate. În realitate, testerii devin mult mai importanți pentru că acum doar ei pot descoperi problemele ascunse, greu sau imposibil de găsit prin teste automate. Ei ajută să crească probabilitatea că totul funcționează corect de la 80+% la aproape 100%.
Testele unitare au câteva caracteristici importante:
Pentru a fi rapide, testele unitare folosesc adesea așa-numitele "duble de testare". La fel cum piloții de avioane învață într-un simulator înainte de a se urca în avion, testele unitare folosesc bucăți de cod care seamănă cu codul de producție, dar în realitate folosesc doar la teste. Stub-urile și mock-urile sunt cele mai întâlnite duble de testare, existând multe altele mai puțin folosite.
Un stub este o dublă de testare care întoarce valori. Stub-ul este similar cu o simulare foarte simplă: atunci când apeși un buton, apare o valoare. În cod, un stub poate arăta astfel:
class PaymentServiceStub implements PaymentService{
public boolean valueToReturnOnPay;
public boolean pay(Money amount){
return valueToReturn;
}
}
class PaymentProcessorTest{
@Test
public void
paymentDoneWhenPaymentServiceAcceptsPayment(){
PaymentServiceStub paymentServiceStub =
new PaymentServiceStub();
paymentServiceStub.valueToReturn = true;
PaymentProcessor paymentProcessor =
new PaymentProcessor(paymentServiceStub);
paymentProcessor.processPayment(
Money.RON(100));
assertPaymentWasCorrectlyPerformed(
paymentProcessor);
}
}
Un mock este o dublă de testare care validează colaborarea între clase. Mock-ul validează apeluri de metode, cu anumiți parametri, de un anumit număr de ori. Din această cauză, un mock poate fi folosit și la validarea apelurilor de metode care nu întorc valori.
class PaymentServiceMock
implements PaymentService{
public boolean payWasCalled;
public Money actualAmount;
public void pay(Money amount){
actualAmount = amount;
payWasCalled = true;
}
}
class PaymentProcessorTest{
@Test
public void
paymentServiceCalledOnPaymentProcessing(){
PaymentServiceMock paymentServiceMock =
new PaymentServiceMock();
PaymentProcessor paymentProcessor =
new PaymentProcessor(paymentServiceMock);
Money expectedAmount = Money.RON(100);
paymentProcessor.
processPayment(expectedAmount);
assertTrue(paymentServiceMock.payWasCalled);
assertEquals(expectedAmount,
paymentServiceMock.actualAmount);
}
}
Dublele de testare pot fi create și folosind framework-uri speciale, cum ar fi mockito pentru Java (a fost portat și pe alte limbaje) sau moq pentru .NET.
class PaymentProcessorTest{
@Test
public void
paymentDoneWhenPaymentServiceAcceptsPaymentWithMockitoStub(){
Money amount = Money.Ron(100);
PaymentServiceStub paymentServiceStub =
mock(PaymentService.class);
when(paymentServiceStub.pay(amount)).
thenReturn(true);
PaymentProcessor paymentProcessor =
new PaymentProcessor(paymentServiceStub);
paymentProcessor.processPayment(amount);
assertPaymentWasCorrectlyPerformed(
paymentProcessor);
}
@Test
public void
paymentServiceCalledOnPaymentProcessingWithMockitoMock(){
Money amount = Money.RON(100);
PaymentServiceMock paymentServiceMock = mock(PaymentService.class);
PaymentProcessor paymentProcessor =
new PaymentProcessor(paymentServiceMock);
paymentProcessor.processPayment(amount);
verify(paymentServiceMock).pay(amount);
}
}
Inițial dublele de testare erau folosite doar în locurile unde era foarte greu să controlezi sistemul sau unde testele erau încetinite de apeluri la sisteme externe. În timp, dublele de testare au ajuns să fie folosite în toate testele unitare, dând naștere metodei "mockiste" de testare unitară. Pentru mai multe detalii, articolul "Mocks aren"t stubs" de Martin Fowler1este edificator.
Testele unitare sunt scrise de programator, în timp ce implementează o funcționalitate.
Din păcate, cel mai întâlnit mod de a scrie teste este cândva după ce a fost terminată implementarea. Rezultatul este că testele sunt scrise având în minte cum ar trebui să funcționeze codul și nu testarea lui.
Test First Programming este o metodă de a scrie teste care implică următorii pași:
Prin aplicarea Test First Programming, programatorii se asigură că scriu teste unitare și că testează ceea ce ar trebui să rezolve, nu implementarea soluției.
Test Driven Development (TDD) poate fi a treia metodă de a scrie teste. De fapt, TDD este o metodă de a face design incremental. Un articol viitor va trata pe larg ce înseamnă și de ce este util TDD.
Durează mai mult când scriu teste!
Studiile de caz2 și experiența personală a arătat că într-adevăr, timpul petrecut strict pe dezvoltarea unei funcționalități crește o dată cu adoptarea unit testing. Aceleași studii au arătat că timpul petrecut pe mentenanță scade drastic, arătând ca unit testing poate aduce o îmbunătățire netă în timpul de dezvoltare.
Acest fapt nu poate schimba percepția programatorului care trebuie să scrie mai mult cod. De aceea, programatorii presupun adesea că per total proiectul merge mai încet din cauza testării automate.
Este bine ca adopția unit testing să se facă cu grijă, incremental, urmărind câteva puncte importante:
Câteva greșeli comune legate de unit testing sunt:
Mai multe detalii despre aceste probleme puteți afla dintr-un blog post de același autor, "5 common unit testing problems" de la adresa http://mozaicworks.com/blog/5-common-unit-testing-problems/.
Unit testing-ul este una dintre metodele pe care un programator o poate folosi cu scopul de a reduce numărul de greșeli pe care le face când scrie cod. Folosit corect, unit testing-ul poate reduce semnificativ timpul petrecut cu repararea bug-urilor din aplicații, reducând încărcarea colegilor care se ocupă de suport și testare și permițând introducerea de noi funcționalități mai repede, ceea ce conduce la competitivitate crescută. Dar unit testing-ul trebuie adoptat cu grijă, urmând practicile din industrie (piramida testelor, folosirea dublelor de testare etc). Ajutorul extern (training și coaching tehnic) poate face diferența între o adopție reușită și una cu probleme.
Un software craftsman stăpânește unit testing și îl folosește atunci când e nevoie să se protejeze de greșeli, fie ale sale fie ale colegilor de echipă. Așa este sigur că poate livra software fără bug-uri chiar și atunci când e sub presiunea timpului. Cu condiția, evident, să învețe unit testing atât de bine încât să îl folosească cu ușurință chiar și atunci când e sub presiune.
"The Art of Unit Testing", Roy Osherove
"xUnit Test Patterns", Gerard Meszaros
"Growing Object Oriented Software Guided by Tests", Steve Freeman, Nat Pryce
2. Cel mai cunoscut studiu de caz legat de unit testing a fost facut la Microsoft: http://collaboration.csc.ncsu.edu/laurie/Papers/Unit_testing_cameraReady.pdf
3. http://martinfowler.com/bliki/TestPyramid.htmlhttp://martinfowler.com/bliki/TestPyramid.html