Unit testing este o metoda folosita pentru a testa fiecare componenta a unui proiect. O unitate este cea mai mica componenta a unei aplicatii. In mod ideal modulele de test sunt independente unele de cealalte. Pentru fiecare unitate se fac teste separate.
Exista si o abordare Test Driven development - TDD in care se scie testul pentru unitate inainte ca sa se scrie codul.
O problema des intalnita in testarea proiectelor este testarea unei parti a proiectului inainte ca alte parti sa fie gata. Se pot folosi pentru asta interfete, numite Stubs, care simuleaza functiile de baza ale obiectului respectiv fara sa efectueze si teste de integritate a datelor sau ale fluxului logic al problemei. Ele sunt des folosite in cursul dezvoltarii unitatilor proiectului care depind de obiectul simulat.
Mockups sunt tot implementarea unor interfete care testeaza mai aprofundat functiile necesare. Ele simuleaza spre exemplu functionarea unui server pentru a putea testa facilitatile clientului si testeaza de asemenea autentificarea clientului inainte ca acesta sa poata efectua anumite tranzactii. Pentru o utilizare mai facila se recomanda folosirea interfetelor si utilizarea lor in functia de testare. O implementare pentru testare este o implementare care contine numai cod de test si imita cat mai bine functionarea viitorului obiect. Mockup-urile sunt utile in multe situatii precum: * cazul cand obiectul in sine nu exista * obiectul real/functia reala ia foarte mult timp sa ruleze * obiectul real este prea dificil de pus in functiune * functia reala returneaza valori nedeterministe si se doreste sa se testeze comportarea cu toate valorile limita * functia reala necesita interactiunea cu utilizatorul si nu se poate folosi in teste automate
Important este ca atunci cand se folosesc obiecte pentru simulare, trebuie sa se ia in seama faptul ca obiectul trebuie sa simuleze cat mai bine realitatea. Exista si facilitati implementate pentru folosirea mockup-urilor in .NET precum NMock, POCMock, .NET Mock Object.
Testele se pot împărți în două categorii:
# teste care verifică corectitudinea unei operații, iar în caz contrar produce o eroare (crapă programul, aruncă excepție)
#* Obs. In multe limbaje de programare există assert
)
#* De exemplu dacă parcurgem elementele unei matrici pe diagonala secundară suma coordonatelor trebuie să rămână constantă.
# teste care verifică că o anumită eroare apare când trebuie
#* in java/python se poate verifica că o anumită funcție aruncă o excepție
#* De exemplu:
# când un proces acceseaza o zonă invalidă de memorie trebuie este aruncat semnalul SIGSEGV
# când se încearcă suprascrierea unui fișier protejat la scriere
# unei funcții îi este pasat un obiect null, deși funcția nu poate opera asupra null
== Platforme de testare ==
# Java: una din cele mai populare platforme de testare este JUnit pentru care găsiți aici un tutorial.
# C#: pentru cam toate platformele .NET există NUnit. Un tutorial gasiti la adresa http://www.nunit.org/index.php?p=quickStart&r=2.4
== Testarea interfețelor grafice ==
Există mai multe utilitare pentru testarea automată a programelor cu interfețe grafice (o listă mai detaliată aveți aici).
=== AutoIt ===
AutoIt este un limbaj de programare asemănător Visual Basic cu un compilator ce rulează pe Windows și care permite (printre altele):
* apelul unor funcții din DLL-uri Win32
* execuția de aplicații (consolă/GUI)
* creare de interfețe GUI (ferestre de mesaje, atenționare, de introducere de date, etc.)
* manipulare sunete
* simulare mișcări de mouse și apăsare taste și combinații de taste
* manipulare ferestre și procese
* manipulare elemente în cadrul unei ferestre
Scripurile pot fi compilate sub forma unor executabile Win32.
Două tutoriale de AutoIt: interacțiune cu notepad și instalare winzip
=== Abbot ===
Abbot este o platformă de testare automată a aplicațiilor GUI scrise în Java. Testele sunt scrise sub forma unor unit-test-uri. Mai multe detalii pe site-ul proiectului.
== Dependency Injection (lucian) ==
DI - procesul prin care sunt furnizare dependințe externe unei componente software. În modelul DI clasele nu-și creează dependințele în mod explicit, ci le primesc ca parametri (dependințele sunt injectate de către creatorul instanței clasei).
Urmând principiul DI, codul scris devine mai ușor testabil întrucât pentru orice test se pot injecta instanțe false
/mock-up
.
Clasa Document
implementată mai jos nu respectă principiul DI întrucât își creează intern o instanță a clasei HtmlClient
:
{{{
class Document {
String html;
Document(String url) {
HtmlClient client = new HtmlClient();
html = client.get(url);
}
}
}}}
Întrucât HtmlClient
este creată intern, atunci când se vor rula testele pentru clasa Document
nu se va putea injecta o implemenetare mai simplă (o machetă - mock-up) a lui HtmlClient
. Astfel toate testele pentru Document
vor depinde de o conexiune activă la internet, vor consuma bandă în mod inutil și vor încetini procesul de testare.
O implementare îmbunătățită:
{{{
class Document {
String html;
Document(HtmlClient client, String url) {
html = client.get(url);
}
}
}}}
Acum putem crea obiecte de test cu
example.org}
injectând în locul lui HtmlClient
o versiune mai simplă care întoarce o aceeași pagină
{{{
class MockupHtmlClient implements HtmlClient {
public String get(String url) {
return ”<html><body> this is a mock-up html document </body></html>”;
}
}
}}}
DI presupune separarea aplicației în două subcomponente:
* o grămadă cu 'obiectele
' aplicației
obiectele implementează logica aplicației,
abstractizează problema,
primesc ca parametri obiectele de care au nevoie,
* o grămadă de apeluri 'new
'
efectuate în obiecte cu rol de mediatori între mai multe obiecte (Factory, Builders, Provider<T>),
crearea grafului de obiecte (relații de dependință între obiecte),
injectează în obiectele create dependințele necesare,
=== Constructori minimali ===
Dacă un constructor creează sau folosește multe obiecte care au timp de viață indelungat se violează principiul DI: acțiunile luate în constructor nu vor putea fi înlocuite cu unele mai simple la testare.
Folosirea unui ServiceLocator
sau a unui Singleton
nu intră în categoria DI atunci când ServiceLocator
-ul/Singleton
-ul este folosit ca o variabilă globală. Dacă acesta este trimis ca parametru, la rulare poate fi înlocuit cu un altul mai simplu. Totuși nici în acest caz nu e mai facil lucrul cu un ServiceLocator
/Singleton
În exemplul următor clasa House
primește un obiect ce urmărește patternul ServiceLocator
, dar nu respectă principiul DI în totalitate, deoarece pentru testarea aplicației se creează în mod inutil un obiect Locator
, când nu aveam nevoie decât de Door
, Window
și Roof
.
{{{
class House {
Door door;
Window window;
Roof roof;
House(Locator locator){
door = locator.getDoor();
window = locator.getWindow();
roof = locator.getRoof();
}
}
class HouseTest {
public void testServiceLocator() {
Door d = new Door(…);
Roof r = new Roof(…);
Window w = new Window(…);
Locator locator = new Locator();
locator.setDoor(d);
locator.setRoof®;
locator.setWindow(w);
House h = new House(locator);
}
}
}}}
Dacă urmărim principul DI, atât procesul normal de creare a unui obiect de tip House
s-a simplificat, cât și testele care
{{{
class House {
Door door;
Window window;
Roof roof;
House(Door d, Window w, Roof r){
door = d;
window = w;
roof = r;
}
}
class HouseTest {
public void testServiceLocator() {
Door d = new Door(…);
Roof r = new Roof(…);
Window w = new Window(…);
House h = new House(d, r, w);
}
}
}}}
Linii generale de urmărit:
* o clasă cere doar obiectele cu care lucrează în mod direct (nu obiecte care să creeze obiectele cu care va lucra),
* folosirea unor construcții de tipul a.getX().getY()
indică o dependință incorect injectată,
* clase cu responsabilități din ambele mulțimi: un obiect cu funcționalitate în logica programului + un obiect care creează alte obiecte.
* se injectează ca parametri în constructori obiecte a căror durată de viață e similară cu cea a obiectului în care se injectează
* se injectează ca parametri în metode obiecte cu durată de viață mai scurtă decât cea a obiectului în care se injectează
Exercițiu: rescrieți clasa Goods după modelul DI și adaptați clasa GoodsTest pentru a folosi noua implementare.
{{{
class Goods {
AccountsReceivable ar;
void purchase(Customer c) {
Money m = c.getWallet().getMoney();
ar.recordSale(this, m);
}
}
class GoodsTest {
void testPurchaseIsHorribleBreaksLoD() {
AccountsReceivable ar = new FakeAR();
Goods g = new Goods(ar);
Money m = new Money(25, USD);
Wallet w = new Wallet(m);
Customer c = new Customer(w);
g.purchase©;
assertEquals(25, ar.getSales());
}
}
}}}
== Regression testing, Integration testing, Fault injection (AC) ==
=== Integration testing ===
Integration testing este faza de testare in care modulele individuale sunt combinate si testate ca un grup. Este faza ce uremaza unit testing-ului si precede testarea de sistem. Deci input-urile sunt modulele ce au trecut de unit testing si iesirea este un sistem integrat gata de testarea de sistem.
Este o faza majora si aproape imposibil de ignorat in creearea unui produs software.
Exista trei abordari majore in unit testing:
* Big Bang
* Bottom Up
* Top down
1) Big Bang - se testeaza direct use case-uri, integrand pe rand toate modulele implicate in use-case.
Avantaje: Viteza cea mai mare.
Dezavantaje: Inaplicabil pe toate produsele, in lipsa unui plan foarte bun de testare creeaza mai multe probleme decat rezolva.
2) Bottom Up - se testeaza modulele combinandu-le in ordine, pornind de la cele cu functionalitati legate, si ajungand la a combina pe toate.
Avantaje: Defecte usor de localizat.
Dezavantaje: Viteza mica, trebuie ca toate modulele de pe acelasi nivel sa fie gata.
3) Top Down - se testeaza modulele incepand de la cele de nivel inalt folosind stuburi si mockupuri.
Avantaje: Usor sa se determine modulele lipsa.
Dezavantaje: Greu de gasit buguri, greu de testat pana cand toate modulele nu sunt gata.
=== Regression testing ===
“Also as a consequence of the introduction of new bugs, program maintenance requires far more system testing per statement written than any other programming. Theoretically, after each fix one must run the entire batch of test cases previously run against the system, to ensure that it has not been damaged in an obscure way. In practice, such regression testing must indeed approximate this theoretical idea, and it is very costly.” – Fred Brooks, The Mythical Man Month (p 122)
Regression testing implica verificarea ca odata cu avansarea in proiect sa nu se piarda functionalitati deja implementate, sau sa se genereze erori noi.
Cea mai simpla si eficienta metoda de regression testing este sa se pastreze toate testele intr-un batch care sa se ruleze periodic, astfel orice bug nou va fi remarcat imediat si poate fi remediat. Desigur, asta implica ca testele respectiva sa poata fi rulate automat.
=== Fault injection ===
'Fault injection' este o metoda de testare software care implica generarea de inputuri care sa duc programul pe cai (in general de error handling) care altfel ar fi parcurse foarte rar in decursul unei testari normale, imbunatatind astfel foarte mult code coverage-ul.
Exista atat software cat si hardware fault injection
HWIFI( Hardware Implemented Fault Injection)
Exista inca din 1970, si implica creearea de scurtcircuite pe placa, generand astfel erori.
SWIFI(Software Implemented Fault Injection)
Se impartea la randul ei in doua mari categorii: Compile time injection si Run time injection
Compile time injection
Modificarea de linii de cod la compilare pentru a genera comportamente eronate.
Ex: a++ poate fi modificat in a–;
Run time injection**
- Coruperea spatiului de memorie al procesului
- Interceptarea syscall-urilor si introducerea de erori in ele
- Reordonarea, coruperea si distrugerea pachetelor de pe retea.
Deși folosite în special pentru optimizări și pentru identificarea bootleneck-urilor din sistem, utilitarele de tip code-coverage și code-profiling pot fi folosite pentru detectarea anumitor tipuri de probleme precum bucle infinite, sincronizare ineficienta etc.
Utilitarele de tipul code coverage sunt folosite în procesul de testare a programelor pentru inspectarea unei părți cât mai mari a programului. Diversele tipuri de mecanisme de tip code coverage sunt folosite pentru a determina ce funcții sunt acoperite la o rulare, ce instrucțiuni sunt apelate, ce fluxuri de execuție sunt parcurse.
Programele folosesc opțiuni speciale de code-coverage. Cu ajutorul acestor opțiuni se pot determina funcțiile sau instrucțiunile des (sau rar) folosite și oferă o imagine a nivelului de testare a anumitor părți dintr-un program.
În general, utilitarele de code coverage sunt privite ca utilitare pentru depanare automată și sunt folosite, de obicei, de inginerii de testare. Depanarea efectivă, cu utilitare de debugging specializate, este realizată, în general de dezvoltatorii care au cunoștință de codul inspectat.
Profilerele sunt utilitare care prezintă informații referitoare la execuția unui program. Sunt utilitare care intră în categoria “dynamic analysis” spre deosebire de alte programe care intră în categoria “static analysis”.
Profilerele folosesc diverse tehnici pentru colectarea de informații legate de un program. De obicei se obțin informații de timp petrecut în cadrul unei funcții (nivel ridicat) sau numărul de cache miss-uri, TLB miss-uri (nivel scăzut).
În general, un program care este “profiled” este instrumentat astfel încât, în momentul rulării, să ofere la ieșire informațiile utile dorite. Spre exemplu, pentru a folosi opțiunile gprof, se folosește opțiunea -pg
transmisă gcc.
* Identificați un utilitar de tipul code-coverage și unul de tipul code profiling pentru fiecare dintre limbajele C, Java, Python, Perl, Ruby, PHP * Enumerați 3 caracteristici ale fiecăruia dintre următoarele utilitare: oprofile, coverity, splint, valgrind.