Elementi di programmazione ad oggetti a. a. 2009/2010 Corso di Laurea Magistrale in Ingegneria Elettronica Docente: Mauro Mazzieri, Dipartimento di Ingegneria Informatica, Gestionale e dell’Automazione Lezione 11 Unit Testing e Test Driven Development Che cos’è il testing? I programmi possono contenere errori Un test consiste nell’esecuzione di un frammento di codice per verificare che funzioni o che continui a funzionare dopo una modifica I test hanno lo scopo di trovare errori in un programma Testare un programma è il modo più affidabile per confermare che funzioni e che continui a funzionare dopo che è stato modificato Modalità di testing Si eseguono molti test, ciascuno su singole porzioni del programma Occorre scegliere accuratamente le porzioni di codice da andare a testare, privilegiando le parti in cui è più probabile che vi siano errori Per ciascuno occorre scegliere accuratamente i dati di input, in quanto non è possibile testare tutte le possibili combinazioni di input Se l’output differisce da quello atteso, si è verificato un errore, solitamente nella parte di codice in esame Come vengono eseguiti i test Dovrebbe essere possibile eseguire spesso i test Dunque, è necessario che siano eseguiti velocemente … quindi, bisogna automatizzarli Gli unit test servono a verificare il comportamento di un oggetto Dunque, verificano l’implementazione della classe di cui l’oggetto è istanza L’idea è di chiamare un metodo e verificare la correttezza del risultato Altri principi dello Unit Testing I test devono essere ripetibili I test devono essere indipendenti, in modo che l’esito di un test non influisca sull’esito degli altri Bisogna eseguire i test dopo ogni cambiamento Se un test fallisce, affrontare subito il problema Esempio: classe Stack #include <vector> template<class T> class Stack { std::vector<T> data; public: void size() const { return data.size(); } void push(const T& t) { data.push_back(t); } T pop() { T result = data.back(); data.pop_back(); return result; } }; Esempio: testiamo la classe Stack #include "Stack.h" #include <iostream> int main() { Stack<int> s; s.push(3); s.push(4); std::cout << s.pop() << " s.pop() << std::endl; system("PAUSE"); return 0; } Ci aspettiamo produca in output: 3 4 " << Framework per lo unit testing Un framework per lo unit testing è una collezione di classi che consente di eseguire unit test Il primo fu SUnit (per Smalltalk) Ogni linguaggio ha il suo “xUnit” Per Java c’è JUnit Per .NET c’è nUnit Per il C++ ce ne sono diversi, nessuno ottimale… Esempio: test di Stack con CppUnitLite #include "TestHarness.h" #include "Stack.h" TEST( creation, Stack ) { Stack<int> s; LONGS_EQUAL(0, s.size()); } TEST( pushpop, Stack ) { Stack<int> s; long a = 3; s.push(a); long b = s.pop(); LONGS_EQUAL(a, b); } Esempio: test di Stack con CppUnitLite #include "TestHarness.h" int main() { TestResult tr; TestRegistry::runAllTests(tr); system("PAUSE"); return 0; } Produce in output: There were no test failures Struttura di un test Un test viene creato con la macro TEST Viene generata una classe Gli identificatori tra parentesi sono il nome del test ed il nome del gruppo È possibile seguire separatamente solo i test di un certo gruppo Nel test viene istanziata la classe da testare Le verifiche vengono compiute attraverso le macro CHECK, LONGS_EQUAL, DOUBLES_EQUAL, FAIL Test Driven Development (TDD) Strategia di sviluppo costruita sull’uso degli unit test Progettare la caratteristica da implementare Se è grande dividerla in parti più piccole Scrivere il test Scrivere il codice Eseguire tutti i test i test passano, effettuare il refactoring Se un test fallisce risolvere il problema immediatamente Se Refactoring Il refactoring è il processo di pulizia del codice che ne migliora la struttura interna senza alterarne il comportamento esterno Per esempio, il refactoring di un metodo porta ad un metodo con lo stesso nome, parametri e tipo di ritorno, ma diverso algoritmo o diversa implementazione dello stesso algoritmo Il refactoring è essenziale per il TDD La prima versione del codice può superare i test ma essere di scarsa qualità Da’ltra parte, prima di effettuare il refactoring il codice deve essere funzionante Esistono interi cataloghi di refactoring comuni: extract method, rename ecc. Problema comune: duplicazione di codice Red, Green, Refactor Scrivere il test Scrivere quel tanto di codice che basta per compilare Eseguire il test: il test fallisce Scrivere il codice nella maniera più semplice possibile, il minimo indispensabile per superare il test Serve a specificare cosa il codice dovrebbe fare Eseguire tutti i test e accertarsi che tutti i test passano Eseguire il refactoring Ripetere per il prossimo test Esempio estremo: fattoriale TEST( test0, fattoriale ) { LONGS_EQUAL(1, f(0)); } int f(int n) { return 1; } TEST( test1, fattoriale ) { LONGS_EQUAL(1, f(1)); } TEST( test2, fattoriale ) { LONGS_EQUAL(2, f(2)); } int f(int n) { return n <= 1 ? 1 : 2; } TEST( test3, fattoriale ) { LONGS_EQUAL(6, f(3)); } int f(int n) { return n <= 1 ? 1 : (n <= 2 ? 2 : 6); } int f(int n) { return n <= 1 ? 1 : n * f(n-1) ; } TEST( test7, fattoriale ) { LONGS_EQUAL(5040, f(7)); } TEST( test11, fattoriale ) { LONGS_EQUAL(39916800, f(11)); } Casi problematici: f(-1) ? f(100) ?