Cum scriem teste unitare?

Bitdefender este un lider recunoscut în domeniul securității IT, care oferă soluții superioare de prevenție, detecție și răspuns la incidente de securitate cibernetică. Milioane de sisteme folosite de oameni, companii și instituții guvernamentale sunt protejate de soluțiile companiei, ceea ce face Bitdefender cel mai de încredere expert în combaterea amenințărilor informatice, în protejarea intimității și datelor și în consolidarea rezilienței la atacuri. Ca urmare a investițiilor susținute în cercetare și dezvoltare, laboratoarele Bitdefender descoperă 400 de noi amenințări informatice în fiecare minut și validează zilnic 30 de miliarde de interogări privind amenințările. Compania a inovat constant în domenii precum antimalware, Internetul Lucrurilor, analiză comportamentală și inteligență artificială, iar tehnologiile Bitdefender sunt licențiate către peste 150 dintre cele mai cunoscute branduri de securitate din lume. Fondată în 2001, compania Bitdefender are clienți în 170 de țări și birouri pe toate continentele. Mai multe detalii sunt disponibile pe www.bitdefender.ro.

Resposabili:

  • Cristi Olaru
  • Cristi Pătrașcu
  • Cristi Popa
  • Darius Neațu
  • Liza Babu
  • Radu Nichita

Cuprins

Obiective

  • De ce facem acest tutorial?
    • este important să testăm codul pe care îl scriem
    • orice modificare nouă a codului nu trebuie să afecteze funcționalitatea veche a codului, prevenim asta prin teste unitare
  • Care este aplicabilitatea practică?
    • sunt automate, odată ce sunt scrise, sunt ușor de rulat și putem vedea ușor problemele din cod
  • Ce vom învăța prin parcurgerea acestui material?
    • ce este, cum se folosește, și cum se scrie un test unitar corect (respectând principiile de scriere)
    • de ce avem nevoie de teste unitare în cadrul unui proiect

Suport tutorial

În acest tutorial vom folosi și scrie teste unitare (eng. unit tests) pentru un set de funcții care conțin algoritmi matematici. Pentru unii algoritmi, testele unitare sunt deja scrise, iar pentru alții vor exista exerciții pentru exersarea skillurilor dobândite în urma parcurgerii acestor tutoriale.

Înainte să începem acest tutorial, descărcăm arhiva cu resurse:

$ cd ~
$ wget https://ocw.cs.pub.ro/courses/_media/programare/tutoriale/unittests/algorithms.zip
$ unzip algorithms.zip
$ tree support/
support/
├── algorithms.c
├── algorithms.h
├── algorithms_test.c
├── macros_test.c
└── Makefile
 
0 directories, 5 files

De ce avem nevoie de teste unitare?

Atunci când lucrăm la un proiect, personal sau din industrie, îi acordăm atenție mai mult timp. Adică, nu dezvoltăm tot proiectul în aceeași zi, ci mai degrabă ne setăm un deadline până când vrem să fie gata și ne organizăm în funcție de data aleasă. Astfel, spargem proiectul în mai multe bucăți, numite și feature-uri, pe care le dezvoltăm pe rând, dar care pot fi dependente de părțile la care am lucrat înainte.

În acest tutorial, vom scrie teste unitare pentru funcții matematice (GCD, LCM, etc.), care pot fi, sau nu, dependente între ele. Pe acest exemplu, algoritmul LCM depinde de gcd, astfel dacă GCD este problematic (a.k.a. are buguri), vom avea buguri și în LCM. Astfel, vrem să ne asigurăm că, înainte să trecem la LCM, GCD este implementat corect.

Avem 2 moduri prin care putem să testăm codul nostru:

  • manual, apelând funcția noastră de mai multe ori, cu parametri diferiți de la apel la apel
    • avantaje:
      • rapid, testare pe moment
    • dezavantaje:
      • nu este o metodă persistentă, dacă nu salvăm comenzile și rezultatele așteptate pentru teste, a doua zi va trebui să refacem testele
  • scriind teste unitare pentru fiecare funcționalitate în parte
    • avantaje:
      • testele rămân scrise undeva, pot fi rulate din nou când e nevoie (e.g. când vrem să revalidăm funcționalitatea în urma unei noi modificări)
      • automate
    • dezavantaje:
      • timpul de dezvoltare mai mare (un feature nu se poate considera terminat din punct de vedere implementare până nu s-au scris toate testele)
      • testele (de orice fel) reprezintă cod scris de programator și acesta trebuie să respecte aceleași standarde ca și codul de feature-uri (e.g. coding style, review, etc)

Ce este un test unitar?

Atunci când testăm o funcție pe care am scris-o, trebuie să ne gândim cu ce date de intrare (parametrii, fișiere de input, etc.) să o apelăm astfel încât să validăm funcționalitatea ei. De cele mai multe ori, există mai multe cazuri de testare. Spre exemplu, când testăm algoritmul gcd, avem următoarele cazuri:

  • ambele numere sunt pozitive:
    • în care numerele sunt prime între ele
    • numerele nu sunt prime între ele, iar cel mai mare divizor comun este mai mic decât ambele numere
    • numerele nu sunt prime între ele, iar numărul mai mic este chiar cel mai mare divizor comun
  • cel puțin un număr este negativ, caz în care algoritmul nu se poate aplica, iar funcția noastră GCD trebuie să trateze această situație

Pentru toate cazurile prezentare mai sus, vom scrie un test unitar. Un test unitar este o funcție care apelează funcția noastră (e.g. GCD) cu date de intrare specifice (parametrii, fișiere, etc.). Dacă avem mai multe cazuri de test, vom scrie mai multe teste unitare, câte unul pentru fiecare caz de test.

Funcția GCD este scrisă în C, declarată în ~/support/algorithms.h și definită în ~/support/algorithms.c și are următoarea semnătură:

int gcd(int first_number, int second_number);

Mai jos este un exemplu de cum arată un test unitar scris pentru funcția gcd:

TEST (gcd, common_case) {
    EXPECT_EQ(9, gcd(81, 153));
}

Detaliem formatul unui test unitar, cum îl compilăm și cum îl rulăm în următoarele secțiuni.

Folosirea frameworkului Google Test

Pentru scrierea testelor unitare putem să scriem un cod sursă C din care să apelăm funcțiile pe care vrem să le testăm sau putem să folosim un framework dedicat pentru scrierea testelor unitare. Avantajul framework-ului este faptul că ne pune la dispoziție metode prin care putem să scriem testele rapid, concentrându-ne astfel pe ce teste scriem, și nu pe cum să le scriem.

În acest tutorial am ales să folosim Google Test, care este un framework dezvoltat pentru scrierea testelor unitare pentru programe C++, însă poate fi folosit pentru programele scrise în C.

De reținut este faptul că Google Test nu este singurul framework disponibil pentru a scrie teste unitare, noi îl folosim în tutorial ca tool pentru a ne îndeplini scopul de a scrie teste unitare. Principiile pe care le prezentăm în acest tutorial sunt aceleași pentru orice framework pe care îl alegem.

Înainte să trecem mai departe, trebuie să instalăm frameworkul la noi pe calculator. Pe Ubuntu rulăm comanda:

$ sudo apt -y install libgtest-dev
[...]

În continuare ne vom acomoda cu formatul testelor unitare și ulterior vom adăuga noi teste pe măsură ce vom avea nevoie de ele.

Folosirea unui test unitar

În această secțiune vom vedea cum rulăm un test unitar deja existent, fără să înțelegem care este formatul lui.

Ne mutăm în directorul ~/support unde găsim următoarele fișiere:

$ cd ~/support
$ ls
total 16
-rw-rw-r-- 1 username username 478 dec 14 15:07 algorithms.c
-rw-rw-r-- 1 username username 316 dec 12 21:29 algorithms.h
-rw-rw-r-- 1 username username 118 dec 14 15:28 algorithms_test.c
-rw-rw-r-- 1 username username 381 dec 14 15:41 Makefile

În fișierele algorithms.h și algorithms.c avem declarările și definițiile funcțiilor matematice pe care vrem să le testăm în acest tutorial.

$ cat algorithms.h
#ifndef __ALGORITHMS_H__
#define __ALGORITHMS_H__
 
int gcd(int first_number, int second_number);
int lcm(int first_number, int second_number);
int binary_search(int *array, int array_size, int to_find);
void bubble_sort(int *array);
char *caesar_cipher(char *original);
int is_prime(int number);
int factorial(int number);
 
#endif

Pentru funcția gcd avem teste unitare definite în fișierul algorithms_test.c:

$ cat algorithms_test.c
#include <gtest/gtest.h>
 
#include "algorithms.h"
 
TEST(gcd, common_case) {
    EXPECT_EQ(9, gcd(81, 153));
}
 
TEST(gcd, common_one) {
    EXPECT_EQ(1, gcd(1, 2346));
}
 
TEST(gcd, common_same_numbers) {
    EXPECT_EQ(55, gcd(55, 55));
}

Momentan vom compila testele deja existente și le vom rula ca să ne acomodăm cu folosirea lor:

$ make algorithms_test
g++ -Wall -Wextra -c -o algorithms.o algorithms.c
g++ -Wall -Wextra -c -o algorithms_test.o algorithms_test.c
g++ -Wall -Wextra -o algorithms_test algorithms.o algorithms_test.o -lgtest -lgtest_main -lpthread
 
$ make run_algorithms_test 
./algorithms_test
-Running main() from /build/googletest-j5yxiC/googletest-1.10.0/googletest/src/gtest_main.cc
-[==========] Running 3 tests from 1 test suite.
-[----------] Global test environment set-up.
-[----------] 3 tests from gcd
-[ RUN      ] gcd.common_case
-[       OK ] gcd.common_case (0 ms)
-[ RUN      ] gcd.common_one
-[       OK ] gcd.common_one (0 ms)
-[ RUN      ] gcd.common_same_numbers
-[       OK ] gcd.common_same_numbers (0 ms)
-[----------] 3 tests from gcd (0 ms total)
-
-[----------] Global test environment tear-down
-[==========] 3 tests from 1 test suite ran. (0 ms total)
-[  PASSED  ] 3 tests.

Outputul de mai sus arată că toate cele 3 teste au trecut cu succes.

Formatul unui test unitar

În această secțiune vedem care este formatul unui test unitar și ce MACRO-uri sunt puse la dispoziție de Google Test pentru a ne ușura munca.

Luăm ca exemplu testul gcd.common_case care verifică dacă funcția gcd() apelată cu parametri 81 și 153 întoarce valoarea 9 (pentru că cel mai mare divizor comun dintre 81 și 153 este 9.. duh?!). Testul arată în felul următor:

$ cat algorithms_test.c
#include <gtest/gtest.h>
 
#include "algorithms.h"
 
TEST (gcd, common_case) { 
    EXPECT_EQ(9, gcd(81, 153));
}
 
[...]

Lucrurile pe care le observăm referitoare la acest test, și pe care trebuie să le urmăm atunci când vom scrie pe viitor noi fișiere de test, sunt următoarele:

  • includem headerul algorithms.h pentru a putea apela funcția greatest_common_divisor de aici
  • includem headerul gtest/gtest.h pe prima linie a fișierului de test pentru a putea folosi MACRO-urile TEST și EXPECT_EQ.
  • MACRO-ul TEST ne permite să scriem un test unitar. Cei 2 parametrii ai săi sunt:
    • gcd, care referă suita de teste din care vrem să facă parte testul nostru. Spre exemplu, aici am numit sugestiv suita de teste GCD pentru că testul verifică funcționalitatea lui gcd.
    • common_case, numele testului care face parte din suita gcd.

În cadrul MACRO-ului TEST, apelăm funcția pe care vrem să o testăm. În acest caz, testăm funcția gcd cu parametri 81 și 153. Verificăm dacă valoarea întoarsă de funcția gcd întoarce valoarea pe care ne așteptăm să o întoarcă (9 în acest exemplu) folosind MACRO-ul EXPECT_EQ. Într-o implementare naivă, EXPECT_EQ este echivalent cu:

if (9 == gcd(8, 153)) {
  printf("SUCCESS\n");
} else {
  printf("FAILED\n");
}

Macro-uri folosite în scrierea testelor

Documentația completă pentru Google Test se află aici, iar toate macro-urile pe care le putem folosi în scrierea testelor se află aici.

În exemplul de mai jos, am folosit EXPECT_EQ / ASSERT_EQ, însă principiile de funcționare sunt aceleași pentru toate MACRO-urile puse la dispoziție de Google Test.

Înainte să trecem prin exemplele de mai jos, compilăm exemplul cu MACRO-uri rulând comanda make macros:

$ make macros_test
g++ -Wall -Wextra -c -o macros_test.o macros_test.c
g++ -Wall -Wextra -o macros_test macros_test.o -lgtest -lgtest_main -lpthread

Majoritatea MACRO-urilor au două forme, pe care le prezentăm în fișierul macros_test.c unde comparăm 2 numere:

  • EXPECT_EQ, care dacă eșuează, nu oprește testul, dar îl declară ca FAILED:
    TEST (macro, expect_eq_example) {
      EXPECT_EQ(8, 9);
      printf("==== Program continues here, but still FAILED ====\n");
    }
  • ASSERT_EQ, care dacă eșuează, oprește rularea testului și îl declară ca FAILED:
    TEST (macro, assert_eq_example) {
      ASSERT_EQ(8, 9);
      printf("==== Program never reaches this line ====\n");
    }

Rulăm comanda make run_macros_test ca să rulăm testul macros_test:

$ make run_macros_test 
./macros_test
Running main() from /build/googletest-j5yxiC/googletest-1.10.0/googletest/src/gtest_main.cc
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from macro
[ RUN      ] macro.expect_eq_example
macros_test.c:4: Failure
Expected equality of these values:
  8
  9
==== Program continues here, but still FAILED ====
[  FAILED  ] macro.expect_eq_example (1 ms)
[ RUN      ] macro.assert_eq_example
macros_test.c:9: Failure
Expected equality of these values:
  8
  9
[  FAILED  ] macro.assert_eq_example (0 ms)
[----------] 2 tests from macro (1 ms total)
 
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (1 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 2 tests, listed below:
[  FAILED  ] macro.expect_eq_example
[  FAILED  ] macro.assert_eq_example
 
 2 FAILED TESTS
make: *** [Makefile:25: run_macros_test] Error 1

Ambele teste au eșuat, însă linia ==== Program continues here, but still FAILED ==== a fost afișată, deși EXPECT_EQ a eșuat:

[  FAILED  ] 2 tests, listed below:
[  FAILED  ] macro.expect_eq_example
[  FAILED  ] macro.assert_eq_example

Pentru a compara cele 2 variante de macro-uri (ASSERT_* vs EXPECT_*) și pentru a folosi versiunea potrivităne gândim la gravitatea situației de eroare:

  • dacă eșuarea înseamnă o eroare fatală a programului, atunci preferăm să folosim ASSERT_*
  • dacă nu, folosim EXPECT_*

Principii de scriere

Până în acest moment, am descris cum arată un test unitar și cum verificăm dacă acesta a trecut sau nu. Mai departe, vedem care sunt principiile de care trebuie să ținem cont atunci când scriem teste unitare.

Atunci când scriem o serie de teste, trebuie sa ne asigurăm că modul în care le scriem respectă următoarele:

  • repetabile: să putem să rulăm ori de câte ori testele, obținând de fiecare dată același rezultat
  • conclusiv: la finalul unui test trebuie să putem să tragem concluzia dacă funcția pe care o testăm funcționează corect pe această rulare particulară
  • izolate: ele nu depind de alte teste scrise anterior; cu alte cuvinte, dacă am rula într-o ordine aleatoare testele, nu ar trebui să fie afectat rezultatul testelor (e.g. orice resurse alocate/modificate într-un test trebuie să fie eliberate/restaurate la final de test)
  • rapide: nu facem operații care nu sunt necesare în testele noastre

Scrierea unui test unitar

În fișierul algorithms_test.c găsim teste scrise pentru funcția gcd. Urmând modelul lor, în această secțiune vom scrie teste unitare pentru funcția binary_search.

Înainte să ne apucăm de scris teste unitare, ne gândim care sunt cazurile pe care vrem să le testăm. Luăm în considerare toate cazurile de test: comune / frecvente, extreme / puțin probabile, cu date de intrare valide / invalide.

În cazul lui binary_search, ne gândim să diversificăm vectorul dat la intrare astfel încât să verificăm că funcția obține rezultatul corect în orice situație:

  • un vector doar cu elemente pozitive, distincte
    • numărul căutat se află în vector
    • numărul căutat nu se află în vector
  • un vector cu elemente duplicate
    • numărul căutat se află în vector
    • numărul căutat nu se află în vector
  • un vector cu toate elementele egale între ele
    • numărul căutat se află în vector
    • numărul căutat nu se află în vector

În continuare vom scrie 6 teste unitare, câte unul pentru fiecare caz menționat mai sus. Aceste 6 cazuri nu sunt singurele posibile, iar dacă în urma rulării testelor, vedem că toate cele 6 cazuri trec, nu înseamnă că putem să tragem concluzia că algoritmul funcționează corect. Pot rămâne cazuri netestate în continuare. Este datoria noastră ca, atunci când scriem teste unitare, să ne gândim la cât mai multe situații posibile.

Vector cu elemente pozitive și distincte

Funcția binary_search are următoarea semnătură:

int binary_search(int *array, int size, int to_find);

În fișierul algorithms_test.c scriem 2 teste unitare pentru cazul în care elementele vectorului sunt pozitive, sortate și distincte: cazul în care elementul căutat se află în vector și cazul în care nu se află în vector.

Alegem pentru aceste 2 teste un vector sortat de 6 elemente distincte {2, 5, 7, 100, 101, 112}. Scriem 2 funcții de test noi care fac parte din suita binary_search și au numele common_case_number_found, respectiv common_case_number_not_found în care declarăm vectorul pentru test:

TEST(binary_search, common_case_number_found) {
    int array[] = {2, 5, 7, 100, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
}
 
TEST(binary_search, common_case_number_not_found) {
    int array[] = {2, 5, 7, 100, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
}

Acum alegem un număr pe care să-l căutăm în vector. Pentru primul test (binary_search.common_case_number_found), trebuie să căutăm un număr care se află în vector. Alegem 7, însă putea fi oricare element care se află în vector. Apelăm funcția binary_search cu parametri de mai jos și știm că pe aceste date de intrare, funcția trebuie să întoarcă valoarea 2, adică indexul din vector la care se află numărul 7:

TEST(binary_search, common_case_number_found) {
    int array[] = {2, 5, 7, 100, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
    EXPECT_EQ(2, binary_search(array, 6, 7));
}

Pentru al doilea caz, trebuie să căutăm un număr care nu se află în vector. Alegem 8, însă putea fi oricare element care nu se află în vector. Apelăm funcția binary_search cu parametri de mai jos și știm că pe aceste date de intrare, funcția trebuie să întoarcă valoarea -1, care indică faptul că numărul căutat nu se află în vector:

TEST(binary_search, common_case_number_not_found) {
    int array[] = {2, 5, 7, 100, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
    EXPECT_EQ(-1, binary_search(array, 6, 8));
}

Compilăm testele pe care le-am scris și le rulăm să vedem dacă implementarea funcționează corect pe cazurile de test alese:

$ make algorithms_test
g++ -Wall -Wextra -c -o algorithms_test.o algorithms_test.c
g++ -Wall -Wextra -o algorithms_test algorithms.o algorithms_test.o -lgtest -lgtest_main -lpthread
 
$ make run_algorithms_test 
./algorithms_test
Running main() from /build/googletest-j5yxiC/googletest-1.10.0/googletest/src/gtest_main.cc
[==========] Running 5 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from gcd
[ RUN      ] gcd.common_case
[       OK ] gcd.common_case (0 ms)
[ RUN      ] gcd.common_one
[       OK ] gcd.common_one (0 ms)
[ RUN      ] gcd.common_same_numbers
[       OK ] gcd.common_same_numbers (0 ms)
[----------] 3 tests from gcd (0 ms total)
 
[----------] 2 tests from binary_search
[ RUN      ] binary_search.common_case_number_found
[       OK ] binary_search.common_case_number_found (0 ms)
[ RUN      ] binary_search.common_case_number_not_found
[       OK ] binary_search.common_case_number_not_found (0 ms)
[----------] 2 tests from binary_search (0 ms total)
 
[----------] Global test environment tear-down
[==========] 5 tests from 2 test suites ran. (0 ms total)
[  PASSED  ] 5 tests.

Ambele teste noi au trecut cu succes. Trecem mai departe cu un alt set de teste.

În cazul în care testele ar fi eșuat, trebuia să verificăm implementarea binary_search, să rezolvăm bugurile astfel încât testele să fie rulate cu succes.

Dacă testele unitare ar fi eșuat, eram siguri că problema vine din funcția binary_search deoarece știm sigur că 7 se află la indexul 2 din vector, respectiv 8 nu se află în vectorul dat.

Nu trebuie să uităm însă că există posibilitatea ca testul pe care l-am scris sa fie greșit. Trebuie să verificăm toate implementările astfel încât să vedem de unde vine problema.

Vector cu elemente pozitive, duplicate

Similar cu exemplul anterior, scriem 2 noi funcții de test în care declarăm un vector de numere întregi pozitive, sortat și cu duplicate, spre exemplu {2, 5, 7, 7, 101, 112}.

Ele fac parte tot din suita binary_search și au numele duplicates_number_found, respectiv duplicates_number_not_found:

TEST(binary_search, duplicates_number_found) {
    int array[] = {2, 5, 7, 7, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
}
 
TEST(binary_search, duplicates_number_not_found) {
    int array[] = {2, 5, 7, 7, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
}

Acum alegem un număr pe care să-l căutăm în vector. Pentru prima funcție, trebuie să căutăm un număr care se află în vector. Alegem 7, însă putea fi oricare element care se află în vector. Apelăm funcția binary_search cu parametri de mai jos și știm că pe aceste date de intrare, funcția trebuie să întoarcă una din valorile 2 sau 3, adică indecșii din vector la care se află numărul 7:

TEST(binary_search, duplicates_number_found) {
    int array[] = {2, 5, 7, 7, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
    int index = binary_search(array, size, 7);
    EXPECT_GE(2, index); // 2 <= index
    EXPECT_LE(index, 3); // index <= 3
}

Pentru al doilea caz, trebuie să căutăm un număr care nu se află în vector. Alegem 2021, însă putea fi oricare element care nu se află în vector. Apelăm funcția binary_search cu parametri de mai jos și știm că pe aceste date de intrare, funcția trebuie să întoarcă valoarea -1, care indică faptul că numărul căutat nu se află în vector:

TEST(binary_search, duplicates_number_not_found) {
    int array[] = {2, 5, 7, 7, 101, 112};
    int size = sizeof(array) / sizeof(array[0]);
    EXPECT_EQ(-1, binary_search(array, size, 2021));  
}

Compilăm testele pe care le-am scris și le rulăm să vedem dacă implementarea funcționează corect pe cazurile de test alese:

$ make algorithms_test
g++ -Wall -Wextra -c -o algorithms_test.o algorithms_test.c
g++ -Wall -Wextra -o algorithms_test algorithms.o algorithms_test.o -lgtest -lgtest_main -lpthread
 
$ make run_algorithms_test 
./algorithms_test
Running main() from /build/googletest-j5yxiC/googletest-1.10.0/googletest/src/gtest_main.cc
[==========] Running 7 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from gcd
[ RUN      ] gcd.common_case
[       OK ] gcd.common_case (0 ms)
[ RUN      ] gcd.common_one
[       OK ] gcd.common_one (0 ms)
[ RUN      ] gcd.common_same_numbers
[       OK ] gcd.common_same_numbers (0 ms)
[----------] 3 tests from gcd (0 ms total)
 
[----------] 4 tests from binary_search
[ RUN      ] binary_search.common_case_number_found
[       OK ] binary_search.common_case_number_found (0 ms)
[ RUN      ] binary_search.common_case_number_not_found
[       OK ] binary_search.common_case_number_not_found (0 ms)
[ RUN      ] binary_search.duplicates_number_found
[       OK ] binary_search.duplicates_number_found (0 ms)
[ RUN      ] binary_search.duplicates_number_not_found
[       OK ] binary_search.duplicates_number_not_found (0 ms)
[----------] 4 tests from binary_search (0 ms total)
 
[----------] Global test environment tear-down
[==========] 7 tests from 2 test suites ran. (1 ms total)
[  PASSED  ] 7 tests.

Ambele teste noi au trecut cu succes, trecem mai departe cu un alt set de teste.

Vector cu toate elementele egale între ele

Ultimul caz pe care am ales să îl verificăm folosește un vector care conține elemente pozitive și egale între ele.

criem 2 noi funcții de test în care declarăm un vector cu toate elementele egale, spre exemplu {5, 5, 5, 5, 5}.

Ele fac parte tot din suita binary_search și au numele equal_number_found, respectiv equal_number_not_found:

TEST(binary_search, duplicates_number_found) {
    int array[] = {5, 5, 5, 5, 5};
    int size = sizeof(array) / sizeof(array[0]);
}
 
TEST(binary_search, duplicates_number_not_found) {
    int array[] = {5, 5, 5, 5, 5}`; 
    int size = sizeof(array) / sizeof(array[0]);
}

Acum alegem un număr pe care să-l căutăm în vector. Pentru prima funcție, trebuie să căutăm un număr care se află în vector. Alegem 7, însă putea fi oricare element care se află în vector. Apelăm funcția binary_search cu parametri de mai jos și știm că pe aceste date de intrare, funcția trebuie să întoarcă o valoare cuprinsă între 0 și 5, adică indexii din vector la care se află numărul 7:

TEST(binary_search, equal_number_found) {
    int array[] = {5, 5, 5, 5, 5};
    int size = sizeof(array) / sizeof(array[0]);
    int index =  binary_search(array, size, 5);
    EXPECT_GE(index, 0); // index >= 0
    EXPECT_LE(index, 5); // index <= 5
}

Pentru al doilea caz, trebuie să căutăm un număr care nu se află în vector. Alegem 8, însă putea fi oricare element care nu se află în vector. Apelăm funcția binary_search cu parametri de mai jos și știm că pe aceste date de intrare, funcția trebuie să întoarcă valoarea -1, care indică faptul că numărul căutat nu se află în vector:

TEST(binary_search, equal_number_not_found) {
    int array[] = {5, 5, 5, 5, 5};
    int size = sizeof(array) / sizeof(array[0]);
    EXPECT_EQ(-1, binary_search(array, size, 8));
}

Compilăm testele pe care le-am scris și le rulăm să vedem dacă implementarea funcționează corect pe cazurile de test alese:

$ make algorithms_test
g++ -Wall -Wextra -c -o algorithms_test.o algorithms_test.c
g++ -Wall -Wextra -o algorithms_test algorithms.o algorithms_test.o -lgtest -lgtest_main -lpthread
 
$ make run_algorithms_test 
./algorithms_test
Running main() from /build/googletest-j5yxiC/googletest-1.10.0/googletest/src/gtest_main.cc
[==========] Running 9 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from gcd
[ RUN      ] gcd.common_case
[       OK ] gcd.common_case (0 ms)
[ RUN      ] gcd.common_one
[       OK ] gcd.common_one (0 ms)
[ RUN      ] gcd.common_same_numbers
[       OK ] gcd.common_same_numbers (0 ms)
[----------] 3 tests from gcd (0 ms total)
 
[----------] 6 tests from binary_search
[ RUN      ] binary_search.common_case_number_found
[       OK ] binary_search.common_case_number_found (1 ms)
[ RUN      ] binary_search.common_case_number_not_found
[       OK ] binary_search.common_case_number_not_found (0 ms)
[ RUN      ] binary_search.duplicates_number_found
[       OK ] binary_search.duplicates_number_found (0 ms)
[ RUN      ] binary_search.duplicates_number_not_found
[       OK ] binary_search.duplicates_number_not_found (0 ms)
[ RUN      ] binary_search.equal_number_found
[       OK ] binary_search.equal_number_found (0 ms)
[ RUN      ] binary_search.equal_number_not_found
[       OK ] binary_search.equal_number_not_found (0 ms)
[----------] 6 tests from binary_search (1 ms total)
 
[----------] Global test environment tear-down
[==========] 9 tests from 2 test suites ran. (1 ms total)
[  PASSED  ] 9 tests.

Toate testele au trecut cu succes, însă acest lucru nu denotă faptul că funcția dă rezultatul corect pentru orice input. La ce alte cazuri de teste vă gândiți?

Exerciții

În continuare au rămăs funcții netestate în algorithms.c:

  • lcm
  • bubble_sort
  • caesar_cipher
  • is_prime
  • factorial

Gândiți-vă la cât mai multe cazuri de test pentru fiecare funcție în parte. Creați o nouă suită pentru fiecare funcție, și în cadrul suitei adăugați toate cazurile de test la care v-ați gândit. Puteți lua în considerare, dar nu vă limitați la, următoarele:

  • cazuri obișnuite de test
  • cazuri limită (numere foarte mari, numere foarte mici)
  • împărțiri la 0 (dacă e cazul)

Funcțiile conțin buguri ascunse care pot fi descoperite prin scrierea de teste unitare. Atunci când descoperiți o problemă, incercati sa o reparati astfel încât funcția de test să ruleze cu succes. PS: tutorialul de debugging poate fi util.

programare/tutoriale/unittests.txt · Last modified: 2022/01/21 02:05 by radu.nichita
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0