ABONAMENTE VIDEO REDACȚIA
RO
EN
×
▼ 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

LANSAREA NUMĂRULUI 85

Prezentări articole și
Panel: Leadership & Management

Marți, 16 Iulie, ora 18:00
Charlie Upstairs, Cluj-Napoca

Înregistrează-te

Facebook Meetup

Conferință

Sponsori

  • ntt data
  • 3PillarGlobal
  • Betfair
  • Telenav
  • Accenture
  • Siemens
  • Bosch
  • FlowTraders
  • MHP
  • Connatix
  • UIPatj
  • MetroSystems
  • Globant
  • Colors in projects