Managementul Proiectelor Software

Laborator 10 - Testare

Bug

O problemă, eroare, defect, posibilitate nedocumentată, greșeală într-un sistem software care împiedică sistemul să funcționeze așa cum este de așteptat este un bug.

Bug report - Un raport care detaliază un bug într-un program, adică cum se manifestă, când, care parte a codului este responsabilă, eventual și o soluție posibilă.

Bug tracking - Bug tracking-ul este un mod organizat de a ține evidența bugurilor și a stărilor acestora (deschis, rezolvat, testat, închis). Acesta poate varia de la forme neorganizate (întâlniri în echipă) la forme organizate (liste de discuții, mail-uri, soluții specializate).

Sistem de bug tracking - O aplicație software care are ca rol ușurarea bug tracking-ului (este o metodă eficientă centralizată și ușor utilizabilă.) Există soluții gratuite, dar și soluții enterprise cu prețuri foarte ridicate. Un sistem de bug tracking este un subtip de sistem de issue tracking.

Sistem de issue tracking - O aplicație software care are ca rol centralizarea și adresarea tuturor cererilor legate de unul sau mai multe produse, de la probleme la schimbări de design și aplicări de patch-uri. Cele mai multe sunt în același timp și bug trackere. În general, sunt mai complexe decât bug trackerele.

Patch - O mică bucată de software creată spre a rezolva problemele și a îmbunătăți performanțele unui program. Aceasta include, dar nu este limitata la: repararea de bug-uri, creșterea vitezei, îmbunătățirea graficii. Unele patch-uri creează și probleme noi.

Patch management - Procesul de a stabili o strategie și a plănui ce patch-uri să fie aplicate, la ce sisteme și când este momentul potrivit pentru aplicarea lor.

Aplicații

GitHub

Așa cum a fost descris în cadrul laboratorului 4, GitHub permite și un management al bug-urilor.

Redmine

Redmine este un tool complex, care oferă și suport pentru bug tracking.
Prin secțiunea “New issue” se pot publica bug-uri, iar aceastea pot fi urmărite.

Bugzilla

Bugzilla este unul din cele mai populare sisteme de urmarire a bugurilor avand o groaza de functionalitati.

Trac

Trac nu este atât de sofisticat ca Bugzilla în ce privește urmărirea bugurilor (adică bug tracking), dar oferă mult mai multe alte facilități precum un navigator de Subversion și un wiki, deci o platformă completă de colaborare între utilizatori și dezvoltatori. De asemenea Trac poate fi extins ușor prin intermediul modulelor, unul dintre cele mai populare fiind cel pentru code review.

Debian bug tracking software

Fiecare bug are o adresă de mail distinctă. Comentariile și controlul bug-urilor se face cu emailuri tipizate.

Launchpad

Launchpad este o aplicație web și un site pentru publicarea proiectelor software (în special cele open source). Printre multe alte facilități interesante dispune și de bug-tracking.

Launchpad poate centraliza rapoartele bugurile din mai multe surse independente și oferă posibilitatea dezvoltatorilor de a discuta despre acel bug într-un singur loc. Launchpad se integrază bine cu trackere externe: Bugzilla, Trac, Sourceforge, Roundup, Mantis, RT și Debian BTS. Launchpad încearcă să elimine patchurile scrise în comentarii prin oferirea posibilității de publicare a unei ramure de cod (branches).
Bugurile pot fi urmărite prin email și atom feeds, fiecare bug având asociată o adresa de email (ex: 249177@bugs.launchpad.net).

Patch-uri

Un fișier patch e un fișier text care descrie diferențele dintre două versiuni ale unui fișier sau dintre două fișiere distincte. Există două formate standard pentru astfel de fișiere: tipul normal și context copiat.
Mai jos este prezentat un format îmbunătățit, Context unificat, care este cel mai întâlnit și mai ușor de vizualizat format.

Structura unui fișier patch cu context unificat:

 --- original_file_name	comentarii (ștampilă de timp, versiunea fișierului în svn/git/etc., etc.)
 +++ modified_file_name	comentarii (idem)
 @@ -start_line__original_file,nr_lines__original_file +start_line__modified_file,nr_lines__modified_file @@
 context_line_before_1
 context_line_before_2
    ...
 context_line_before_n
 -  original_line_1
 -  original_line_2
    ...
 -  original_line_m
 +  modified_line_1
 +  modified_line_2
    ...
 +  modified_line_p
 context_line_after_1
 context_line_after_2
    ...
 context_line_after_n

Exemplu de fișier patch cu context unificat:

 --- v1.c	2008-11-18 02:34:38.000000000 +0200
 +++ v2.c	2008-11-18 02:38:14.000000000 +0200
 @@ -261,8 +261,18 @@
      if (build_key(argv[1]))
          return 2;
  
 -    display("original  message: [%s]\n", msg, blocks);
 -
 +    //display("original  message: [%s]\n", msg, blocks);
 +    if (blocks)
 +    {
 +        char c;
 +        char * fmt = "original  message: [%s]\n";
 +        int len = blocks;
 +        c = msg[len-1];
 +        if (c == 'X')
 +            msg[len-1] = '\0';
 +        printf(fmt, msg);
 +        msg[len-1] = c;
 +    }
  
      apply(msg, blocks, normalize);
      encrypt((unsigned short*) msg, blocks/2);

Creare patch

diff

'diff' e un program cu care se pot crea fișiere de tip patch având la dispoziție fișierul original și fișierul modificat. diff poate crea fișiere patch în oricare din cele trei formate standard:

  • normal
     diff origial_filename modified_filename
  • context unificat
    • pentru a crea un fișier patch cu un număr implicit de linii de context unificat:
       diff -u origial_filename modified_filename
  • pentru a crea un fișier patch cu x linii de context unificat:
     diff -Ux origial_filename modified_filename

git format-patch

git poate genera fișiere de patch care descriu diferențele între două commit-uri.
Implicit git format-patch crează un fișier diff în format unificat (cel mai răspândit format în lumea open-source) și îl scrie la stdout. Pentru a-l scrie într-un fișier pe disc, trebuie redirectată ieșirea către un fișier:

 git format-patch [optional parameters] > file.patch

Utilizare patch

Un fișier patch nu este folosit doar pentru a vizualiza diferențele între două versiuni ale unor fișiere, ci și pentru a transmite un anumit set de modificări de la un utilizator la altul pentru a fi aplicate.

Utilitarul standard cu care se aplică patch-uri se numește patch.

Modul cel mai întâlnit de utilizare este:

 patch -pNUM < filename.patch

patch își ia fișierul din stdin, de aceea trebuie redirectat fișierul de intrare.

În fișierul filename.patch, diff sau git format-patch va scrie și numele fișierelor pe care le-a comparat și din conținutul cărora a extras diferențele. E posibil ca diff sau git format-patch să fie rulate într-un alt director față de cel în care se va rula patch. Cu opțiunea -pNUM se specifică numărul de nivele din calea specificată în fișierul filename.patch care vor fi ignorate când se va încerca să se determine.

De asemenea, un patch poate fi aplicat cu comanda:

git am filename.patch

Test Cases

Pentru o testare de calitate, este important să existe o planificare riguroasă a testării încă din faza de proiectare sau development. Pe măsură ce se conturează definițiile modulelor, entităților de date, obiectelor, claselor, funcțiilor, etc. este recomandabil să se scrie și “scenarii” de testare ale acestora, fie top-down, fie bottom-up.

În industria software, “scenariile” de test se numesc test cases.

Un exemplu de test case:

  • trebuie verificată funcționarea unei pagini de login care conține un input de user name și unul de parolă
    1. click pe “Login” fără a completa user/pass → trebuie să rămân în pagină și să se afișeze un mesaj de eroare
    2. completat user corect, parola incorectă sau nulă, click pe “Login” → trebuie să rămân în pagină și să se afișeze un mesaj de eroare
    3. completat user corect, parola corectă, click pe “Login” → trebuie să fiu corect autentificat
    4. completat user corect, parola corectă, tastat “Enter” → trebuie să fiu corect autentificat

Există “testare pozitivă” și “testare negativă”, concretizată în positive test cases și negative test cases. Testarea pozitiva înseamnă verificarea faptului că sistemul face ceea ce trebuie să facă. Testarea negativă înseamnă verificarea faptului că sistemul nu face ceea ce nu trebuie să facă.

Pe cazul anterior:

  • testare pozitivă: se verifică faptul că la user/pass corecte se face login iar la user/pass incorecte se afișează un mesaj de eroare
  • testare negativă: se verifică faptul că la user/pass corecte NU se afișează un mesaj de eroare iar la user/pass incorecte NU se face login și NU se “sparge” sistemul

În principiu, cele două approachuri sunt echivalente, însă în practică testarea pozitivă se referă la funcționarea “normală” a sistemului, iar testarea negativă la “corner cases”. De exemplu, pentru testarea unui feature critic ca time to market dar non-critic ca și calitate (ex. twitter), se va prefera testarea pozitivă, care asigură că sistemul funcționează corect pentru cei mai mulți utilizatori. Pentru testarea unui feature critic ca și calitate (ex. online banking) se va insista pe teste negative, ex. se va încerca “spargerea” sistemului prin combinații incorecte.

Există, ca în orice alt domeniu, tool-uri open source pentru managementul test cases:

Black Box Testing

Se aplică în cazul în care pentru programul ce trebuie testat sursele nu sunt disponibile, ci doar interfața de acces (binarul, o interfața implementată de clasa testată, etc).
Cum se testează:
Se aplică un set de intrări, iar ieșirile sunt comparate cu un set de ieșiri corecte.

  • Intrări - un set de intrări ce vor fi trimise programului pentru execuție. Intrările nu trebuie neapărat să fie niște fișiere ci pot fi niște apeluri de funcții sau un alt program ce simulează alte module ce interacționază cu programul testat.
  • Ieșiri - pentru fiecare intrare se definește un set de ieșiri posibile cu care se testează ieșirea programului. În multe din cazuri ieșirea e deterministică și trebuie doar comparate ieșirea programului cu ieșirea așteptă, iar în alte cazuri este necesară scrierea unui program.

Exemplu Testarea unui program care la intrare primește un string pe care vrea să îl parseze în

  • număr întreg
    • limitele inferioare și superioare
    • câteva constante mici (0, -1, 1)
    • câteva constante mari
    • întregi negativi
    • caractere invalide (de exemplu, pentru baza 10 nu putem avea A)
    • numere care au 0 în față (dar excesiv de lungi gen: 00000000000000000000000000000666)
    • același lucru pentru diferite baze (dacă sunt suportate)
  • float
    • ca mai sus, dar în principal trebuie ținut cont de valorile speciale definite în standardul IEEE 754
    • -0.0 != 0.0
    • Nan, -Nan
    • inf, -inf
  • un șir de caractere
    • limbajul acceptat (LFA style)
  • un vector
    • se aplică cazurile de la numere întregi (pentru 1 element)
    • 0 elemente, 1 element, 2 elemente, 5 elemente, număr maxim de elemente
    • caractere de despărțire / elemente invalide

Boundary testing

Boundary testing sau boundary value analysis este o metodă de proiectare a suitelor de teste pentru cazurile în care se folosesc valori la limită acceptate de program. În general, accentuează testarea “corner case”-urilor.
Unele teste care fac parte din suita boundary testing sunt “stress tests”.

În general, boundary value analysis se realizează în doi pași:

  1. identificarea claselor de echivalență
  2. proiectarea suitelor de test

Primul pas înseamnă, de obicei, partiționarea valorilor posibile în clase valide și invalide.
Exemplu Un program care primește valori pozitive până în 99, va avea trei clase:

  • clasa validă (0 < n ⇐ 99)
  • prima clasă invalidă (n ⇐ 0)
  • a doua clasă invalidă (n > 99)

Al doilea pas înseamnă proiectarea unei suite de teste care vor selecta anumite valori care să verifice reacția programului la valori valide sau invalide.
Exemplu Dacă un program primește valori în domeniul [-999,999], atunci o suită posibilă de test ar fi:

  • testare valoare -999
  • testare valoare 999
  • testare valoare -998
  • testare valoare 998

În general, testele trebuie să includă prima și ultima valoare posibilă. De asemenea, se recomandă testarea de condiții extreme de intrare sau ieșire (valori foarte mici, foarte mari, invalide etc.)

Problemele pe care le detectează boundary testing sunt, de obicei:

  • folosirea incorectă a operatorilor relaționali (mai mic, mai mare etc.)
  • folosirea de valori constante inițializate incorect
  • erori de calcul care ar putea realiza overflow sau wrap-around în cazul conversiei între diverse tipuri de date

Link-uri utile

Unit testing

Unit testing este o metodă folosită pentru a testa fiecare componentă a unui proiect. O unitate este cea mai mică componentă a unei aplicații. În mod ideal modulele de test sunt independente unele de celelalte. Pentru fiecare unitate se fac teste separate.

Există și o abordare Test Driven Development - TDD în care se scrie testul pentru unitate înainte de scrierea codului.

Stubs

O problemă des întâlnită în testarea proiectelor este testarea unei părți a proiectului înainte ca alte părți să fie gata. Se pot folosi pentru asta interfețe, numite Stubs, care simulează funcțiile de bază ale obiectului respectiv fără să efectueze și teste de integritate a datelor sau ale fluxului logic al problemei. Ele sunt des folosite în cursul dezvoltării unităților proiectului care depind de obiectul simulat.

Mockup

Mockups sunt tot implementarea unor interfețe care testează mai aprofundat funcțiile necesare. Ele simulează spre exemplu funcționarea unui server pentru a putea testa facilitățile clientului și testează de asemenea autentificarea clientului înainte ca acesta să poată efectua anumite tranzacții. Pentru o utilizare mai facilă se recomandă folosirea interfețelor și utilizarea lor în funcția de testare. O implementare pentru testare este o implementare care conține numai cod de test și imită cât mai bine funcționarea viitorului obiect. Mockup-urile sunt utile în multe situații precum:

  • cazul când obiectul în sine nu există
  • obiectul real/funcția reală ia foarte mult timp să ruleze
  • obiectul real este prea dificil de pus în funcțiune
  • funcția reală returnează valori nedeterministe și se dorește testarea comportării cu toate valorile limită
  • funcția reală necesită interacțiunea cu utilizatorul și nu se poate folosi în teste automate

Important este ca atunci când se folosesc obiecte pentru simulare, trebuie să se țina cont de faptul că obiectul trebuie să simuleze cât mai bine realitatea. Există și facilități implementate pentru folosirea mockup-urilor în .NET precum NMock, POCMock, .NET Mock Object.

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 implică verificarea ca odată cu avansarea în proiect să nu se piardă funcționalități deja implementate, sau să se genereze erori noi.

Cea mai simplă și eficientă metodă de regression testing este să se păstreze toate testele într-un batch care să se ruleze periodic, astfel orice bug nou va fi remarcat imediat și poate fi remediat. Desigur, asta implică ca testele respective să poată fi rulate automat.

Fault injection

'Fault injection' este o metodă de testare software care implică generarea de input-uri care să ducă programul pe căi (în general de error handling) care altfel ar fi parcurse foarte rar în decursul unei testări normale, îmbunătățind astfel foarte mult code coverage-ul.

Există atât software cât și hardware fault injection.

HWIFI(Hardware Implemented Fault Injection)

Există încă din 1970, și implică crearea de scurtcircuite pe placă, generând astfel erori.

SWIFI(Software Implemented Fault Injection)

Se împarte în două mari categorii

  1. Compile time injection
  2. Run time injection

Compile time injection

Modificarea de linii de cod la compilare pentru a genera comportamente eronate.
Ex: a++ poate fi modificat în a–;

Run time injection

  • Coruperea spațiului de memorie al procesului
  • Interceptarea syscall-urilor și introducerea de erori în ele
  • Reordonarea, coruperea și distrugerea pachetelor de pe rețea.

Platforme de testare

  1. Java: una din cele mai cunoscute platforme de testare este JUnit pentru care găsiți aici un tutorial.
  2. C#: pentru cam toate platformele .NET există NUnit. Un tutorial gasiți la adresa http://www.nunit.org/index.php?p=quickStart&r=2.4
  3. Python unittest pentru Python.

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.

Code coverage, Code profiling

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 ineficientă etc.

Code coverage

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.

Code profiling

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.

Exerciții

  • Împărțiți-vă în 3-4 echipe de câte 2-3 persoane.
  • Fiecare echipă lucrează, folosind unit testing în Python, la una dintre următoarele programe.
    • Fiecare echipă își va defini specificațiile: formatul de intrare, cum vor fi afișate/transmise rezultatele, ce interfață va folosi programul.
    1. Joc X și 0.
    2. Aplicație care calculează valoarea unei expresii în algebră booleană (de forma x*y+x*!z+z*!+x*!y*t).
      • * – ȘI logic
      • + – SAU logic
      • ! – negat
    3. Aplicație care calculează valoarea unei operații cu matrice pătratice (de forma A*t(B) + det(A)*B + A*tr(B)). Operațiile sunt:
      • * – înmulțire
      • + – adunare
      • det(A) – determinantul matricei A
      • tr(A) – urma matricei A (trace)
      • t(A) – transpusa matricei A
    4. Un generator de hartă pentru jocul Battleship.
  • Pașii pe care îi veți urma sunt:
    1. Gândiți-vă la aplicație și proiectați metodele folosite.
    2. Creați infrastructura de unit testing (funcțiile setUp, tearDown).
    3. Creați un test (sau mai multe) pentru o metodă dată.
    4. Implementați metoda.
    5. Rulați testul/testele și verificați validitatea implementării.
  • Ca punct de plecare pentru unittest puteți porni de aici. Intrați în directorul magicfolder/tests/. Testul test_init.py este un test destul de simplu.

Bibliografie

laboratoare/laborator-10.txt · Last modified: 2012/11/23 15:43 by andrei.maruseac