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) ?