ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 12
Abonament PDF

Din uneltele artizanului software: Unit Testing

Alexandru Bolboacă
Agile Coach and Trainer, with a focus on technical practices
@Mozaic Works



PROGRAMARE

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:

  • testarea exclusivă a funcționalităților noi, cu speranța că cele vechi nu s-au modificat;
  • analiza impactului modificărilor și testarea exclusivă a funcționalităților afectate;
  • utilizatorii vor testa într-o perioadă de stabilizare.

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:

  • Scăderea mulțumirii utilizatorilor și la apariția detractorilor. În afaceri, la fel ca în viață, e greu să câștigi încrederea unei persoane dar e foarte ușor să o pierzi.
  • Creșterea costurilor cu suportul. Fiecare bug raportat de utilizator înseamnă timp petrecut pentru înțelegerea lui, rezolvarea, testarea și punerea în producție a noii versiuni. Costurile se acumulează în: call center, dezvoltare, testare, operațional.
  • Costul de oportunitate: cât timp echipa de dezvoltare rezolvă bug-uri, competiția poate scoate funcționalități noi care vor atrage utilizatorii. Rezolvarea bug-urilor este echivalentă din punctul de vedere al afacerii cu alergatul pe loc.

Dar dacă...

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.

Unit testing

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:

  • fiecare test validează un comportament din aplicație;
  • rulează foarte repede, maxim în câteva minute;
  • sunt foarte scurte și ușor de citit;
  • rulează la apăsarea unui buton, fără configurări suplimentare.

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:

  • crearea unui design pentru implementarea funcţionalităţi
  • crearea minimului de cod necesar (compilabil, dacă limbajul folosit este compilat) pe baza design-ului
  • scrierea unuia sau mai multor teste care codează ceea ce trebuie să facă design-ul; testele vor pica în acest moment
  • implementarea codului care face testele să treacă.

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.

Cum încep?

Este bine ca adopția unit testing să se facă cu grijă, incremental, urmărind câteva puncte importante:

  • Clarificarea conceptelor legate de unit testing înainte de a începe scrierea de teste.Programatorii trebuie să poată "mânui" fără teamă unelte precum: stub-uri, mock-uri, teste de stare, teste de colaborare, teste de contract, dependency injection. De asemenea, programatorii trebuie să înțeleagă ce cazuri merită și trebuie testate.Există câteva modalități prin care programatorii reușesc să stăpânească aceste concepte:
  • training specializat pe unit testing. Mozaic Works oferă un asemenea curs (http://bit.ly/unit-testing-workshop) care a avut constant feedback de peste 9.25/10 de la participanți.
  • pair programming între un tester și un dezvoltator.
  • pair programming între un dezvoltator experimentat în unit testing și unul începător. Dezvoltatorul experimentat poate fi și un coach tehnic extern.
  • documentarea din cărți (vezi la final cărţii recomandate), de pe internet sau prin participarea la evenimente de comunitate.
  • participarea la conferințe unde se discută concepte de unit testing.
  • Un coach tehnic poate lucra cu programatorii, ajutându-i să transfere informațiile teoretice în practica de zi cu zi astfel încât productivitatea să fie cât mai puțin afectată;
  • Testarea automată în primul rând a celei mai importante părți din aplicație și apoi a funcționalităților cu cel mai mare risc de greșeală;
  • Folosirea strategiei de testare de tip "Piramida testelor"3pentru a elimina cât mai multe greșeli cu putință;
  • În cazul în care există mult cod (legacy code), este recomandată învățarea unor tehnici suplimentare pentru a scrie teste pe cod existent. Mai multe detalii într-un articol viitor.

Greșeli comune

Câteva greșeli comune legate de unit testing sunt:

  • Scrierea multor teste de integrare(care implică mai multe clase sau module) lente și fragile în detrimentul testelor unitare mici, rapide și ușor de întreținut
  • Abandonarea dublelor de testare, sau folosirea lor în scopuri pentru care nu au fost create. Dublele de testare ajută la obținerea unor teste scurte și rapide.
  • Numele testelor nu exprimă comportamentul testat. Numele testului poate da foarte multe informații atunci când testul pică.
  • Folosirea intensivă a debugger-ului pe teste. Testele bine scrise vor spune imediat unde este problema în cazul în care pică. Debugging-ul este în continuare util în situații exotice.
  • Cod de testare neîngrijit. Codul de testare este cel puțin la fel de important ca și codul de producție, și trebuie întreținut cu aceeași grijă.

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/.

Concluzie

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.

Cărți recomandate

"The Art of Unit Testing", Roy Osherove

"xUnit Test Patterns", Gerard Meszaros

"Growing Object Oriented Software Guided by Tests", Steve Freeman, Nat Pryce


1. http://martinfowler.com/articles/mocksArentStubs.html

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

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects