Testing e Debugging
1
Perché? Che cosa? Quando?
• GOAL: software con zero difetti …
MA impossibile da ottenere e garantire
• Necessaria una attenta e continua
VERIFICA
• Tutto deve essere verificato: documenti
di specifica, di progetto, dati di
collaudo, ….programmi
• Si fa lungo tutto il processo di sviluppo,
NON solo alla fine!
2
Terminologia
• Verifica (verification): insieme delle attività volte
a stabilire se il programma costruito soddisfa le
specifiche (non solo funzionali)
– did we build the program right?
• si assume che le specifiche esprimano in modo esauriente i
desiderata del committente
• Testing: particolare tipo di verifica sperimentale
fatta mediante esecuzione del programma,
selezionando alcuni dati di ingresso e valutando
risultati
3
Terminologia
• Debugging: localizzare errori (difetti) nel
codice (il testing ne rivela la presenza ma non
li localizza)
• Programmazione difensiva: insieme di
tecniche di programmazione che cercano di
evitare di introdurre errori, aumentano
probabilità di correttezza e facilitano verifica e
debugging
• Convalida: stabilire se il programma soddisfa
le “aspettative” del committente
(indipendentemente dalle sue specifiche)
Attenzione: terminologia non
4
standardizzata!
Testing
• Si fanno esperimenti con il comportamento
del programma allo scopo di scoprire
eventuali errori
– si campionano comportamenti
– come ogni risultato sperimentale, fornisce
indicazioni parziali relative al particolare
esperimenti
• programma provato solo per quei dati
• GOAL: trovare "controesempi"
• Tecnica dinamica rispetto alle verifiche
statiche fatte dal compilatore
5
Testing
• Testing esaustivo (esecuzione per tutti i possibili
ingressi) dimostra la correttezza
• p.es. se programma calcola un valore in base a un valore di
ingresso nel range 1..10, testing esaustivo consiste nel
provare tutti i valori: per le 10 esecuzioni diverse si verifica
se il risultato è quello atteso
Impossibile da realizzare in generale:
• p.es. se programma legge 3 ingressi interi nel range 1..1000
e calcola un valore, testing esaustivo richiede 109 esecuzioni!
– per programmi banali si arriva a tempi di esecuzione pari al
tempo passato dal big-bang
6
Testing
• Program testing can be used to show
the presence of bugs, but never to
show their absence. (Dijkstra 1972)
• Obiettivo: trovare dati di test che
massimizzino la probabilità di trovare
errori
7
Criteri di test
• È cruciale la scelta di opportuni valori (dati o
casi di test) "sufficienti a convincerci" che il
programma è corretto
p.es.: eseguire il programma con i valori 1, 10 e 5.
• In base a che cosa si determinano i casi di
test?
– in base alla specifica (test black-box o funzionale;
esiste anche un test white/glass-box, di cui qui non
parliamo)
e quando?
– idealmente, nel momento in cui si scrive la
specifica del modulo
• In base a quali criteri?
8
Testing funzionale
• Proprietà
– esprime il punto di vista di chi vuole/deve
usare il modulo
– esito del test comprensibile da chi non
conosce l’implementazione
• Varie tecniche per generare dati di test a
partire da specifiche
9
Test funzionale: combinazioni
proposizionali (1)
ESEMPIO
• Combinare i vari casi alternativi espressi da una specifica
static boolean isPrime (int x)
//@ensures (* \result <==> x è primo*)
–
Scegliere dati di test primi e non primi. Es. 5 e 8
10
Test funzionale:
combinazioni proposizionali (2)
In generale, possiamo identificare le parti in alternativa
di una specifica espressa come formula di logica
proposizionale usando l’operatore ||
static float sqrt (float x, float epsilon)
//@requires x >= 0 && .00001 < epsilon < .001 ;
//@ensures x-epsilon <= \result * \result <= x+epsilon ;
– REQUIRES congiunzione di x >= 0 con .00001<epsilon<.001
•
Parte x >= 0 equivalente a x=0 || x>0
– Combinazioni ottenibili:
•
x = 0 && .00001 < epsilon < .001
•
x > 0 && .00001 < epsilon < .001
11
Il metodo seguito
• Si esamina la clausola requires e, se
possibile, la clausola effects
• Si partiziona il dominio di ingresso in
sottoinsiemi come specificato da essa,
riducendo la formula proposizionale in forma
– x1&&x2&&…. || y1&&y2&&… ym || …
• NB: metodo applicabile a clausole requires
ma non sempre a clausola ensures
– in questo caso non conoscendo codice impossibile
prevedere se \result*\result <x o =x o >x: come
scegliere x?
12
Es. applicato a effects
• Es. static int maxOfThree (int x, int y, int z) {
//@ensures (* restituisce il valore massimo fra x, y, z *)
•
Ci sono tre alternative: il massimo è x, è y, o è z
•
Es. static int maxOfThree (int x, int y, int z) {
//@ ensures \result == x && x>=y && x>=z ||
\result == y && y>=x && y>=z ||
\result == z && z>=x && z>=y
•
•
Ci sono tre alternative: il massimo è x, è y, o è z
Casi di test ricavabili da ensures:
– Un caso in cui il massimo è x, p. es. (5,3,0)
– Un caso in cui il massimo è y, p. es. (7,11,2)
– Un caso in cui il massimo è z, p. es. (7,10,12)
13
Testing funzionale (cont.)
• Altre volte possibile e necessario usare
clausola ensures
static boolean isPrime (int x)
//@ensures \result == true iff x prime
– Scegliere dati di test primi e non primi
14
Casi eccezionali
• Testare non solo il comportamento normale ma anche le
eccezioni
//@ensures a!=null &&
//@ (\exists int i; 0<=i && i<a.length; x==a[i]) && a[\result]==x ;
//@ signals (NotFoundException e)
//@
(\forall int i; 0<=i && i<a.length; x != a[i]);
//@ signals (NullPointerException e) a == null
static int search (int [] a, int x)
throws NotFoundException, NullPointerException
– Testare search con array a null, con x non in a, con x in a
• ?? testare anche con input che non soddisfa clausola
requires??
– NO, se metto requires è responsabile il cliente
– se invece metto eccezione, devo anche testare la via cha la genera
è bene evitare la requires e definire funzioni totali
15
Testing con valori limite
(boundary values)
• Se valore dell’input può stare in un intervallo,
testare estremi dell’intervallo e combinare
valori limite
• Esempi:
– valori estremi per i numeri (max. int
ammissibile)
– sqrt con radicando = 0
– stringa: vuota o di 1 carattere
– array: array vuoto o di un elemento
– elaborazioni con array: considerare valori
estremi degli indici
16
Altri esempi
– Triangoli identificati da vertici:
• tre punti allineati
• due punti coincidenti
• tre punti coincidenti
• triangolo rettangolo
• un vertice nell’origine o sugli assi
• ….
17
Esempio
//@ensures (*\result è il massimo fra x, y, z *)
static int maxOfThree (int x, int y, int z)
• Casi limite:
• x = y = z: p.es. 3, 3,3
• x=y !=z
• ecc.
18
Esempio: casi limite con alias
• Due parametri si riferiscono a due oggetti
mutabili, dello stesso tipo
• Considerare casi in cui coincidono, anche se non
previsto esplicitamente dalle specifiche
//@ensures(* removes all elements of v2 and appends
//@them in reverse order to the end of v1 *)
static void appendVector(Vector v1, Vector v2){
while (v2.size() > 0) {
v1.addElement(v2.lastElement());
v2.removeElementAt(v2.size()-1); }
}
NON è vietato che v1 e v2 siano lo stesso Vector:
testando questo caso si trova un errore
19
Testing di astrazioni sui dati
• Si effettua test per tutte le operazioni
del tipo di dato, MA sfruttando sinergie
tra metodi costruttori e modificatori e
metodi osservatori
• Caso di studio: l’astrazione IntSet
20
Specifica di IntSet
public class IntSet {
/*OVERVIEW: insiemi di interi illimitati e modificabili; per es.: {1, 2, 10, -55} */
//costruttori:
//@ensures (\forall int y;;!this.isIn(y));
public IntSet(){
}
//metodi mutators:
//@ ensures this.isIn(x) && (\forall int y; x!=y;
//@
\old(this.isIn(y)) <==> this.isIn(y));
public void insert(int x){ }
//@ ensures !this.isIn(x) && (\forall int y; x!=y;
//@
\old(this.isIn(y)) <==> this.isIn(y));
public void remove(int x){ }
//@ensures (*\result è true sse x è fra gli elementi di this*);
public boolean isIn (int x){}
//@ ensures (*\result è cardinalità di this *);
public int size(){}
21
Esempio: Test di IntSet
• Testing funzionale:
– valori limite: generare IntSet con 0, 1 o 2 el.
– per ognuno testare isIn (risultato false e
true), size, elements
– testare size dopo insert e remove, con aggiunta
o cancellazione di elemento presente o assente
– testare elements per insiemi di 0, 1, 2, elementi
22
Verificare RI e specifiche
• Quando si testano classi, casi di test diventano molto
numerosi.
• Difficile capire se i risultati sono corretti, e nel caso in
cui siano scorretti qual’e la causa dell’errore.
• Per automatizzare, allora verificare sistematicamente
RI (es. chiamando un metodo repOK() dopo ogni
operazione di costruzione e modifica)
• Es. Aggiungere repOk() (verifica invariante)
– quando si esce da IntSet(), remove(), insert()
• Se possibile anche verifica delle pre e postcondizioni
• Tutto questo aiuta ad automatizzare il testing e
successivamente semplifica il debugging
23
Esempio: semplificare post
• Verifica della postcondizione di insert è
difficile perche’ richiede di verificare che
tutti gli elementi già presenti
nell’insieme non sono eliminati
• Piu’ facile verificare solo che la
cardinalità dell’insieme sia corretta:
isIn(x) &&
(\old(isIn(x))==> size(x)==\old(size(x))
||
\old(isIn(x))==>size(x)==1+\old(size(x)))
24
Test delle gerarchie di tipi
• Dati di test funzionale per sottotipo
devono includere quelli del supertipo; in
generale sottotipo testato con
– dati di test funzionali per supertipo, con in
più chiamate del costruttore del sottotipo
– dati di test funzionali aggiuntivi
caratteristici del sottotipo
25
Test di unità e di integrazione
• Test di unità
– verifica di un singolo modulo isolatamente
• Test di integrazione
– verifica di corretta interazione dei moduli
• Test di integrazione più difficile
– comportamento da testare più complesso
– maggiore dimensione del codice
– spesso interazione poco/mal specificata, e
moduli di tipo e provenienza disomogenea
Conviene prima test di unità e poi test di integrazione (divide et impera)
26
Test di unità
JUnit
27
Esecuzione dei test
• Quando si testa un programma è importante
definire esattamente i risultati attesi (si parla
di oracolo)
• Si può automatizzare sia l'esecuzione dei test
che il controllo dei risultati (Junit)
• Junit (http://junit.org/index.htm)
– si basa sull'idea "first testing then coding"
– "test a little, code a little, test a little, …
28
Junit: esempio 1
import junit.framework.*;
(1)
public class SimpleTest extends TestCase { (2)
public SimpleTest(String name) {
(3)
super(name);
}
public void testSimpleTest() {
(4)
int answer = 2;
assertEquals((1+1), answer);
(5)
}
}
29
Spiegazioni
•
•
•
•
•
importazione delle classi definite da Junit
va ridefinita la classe TestCase
costruttore del nostro specifico test case,
che ha un nome (ne vedremo l'uso più avanti)
definizione di uno specifico test interno al
test case
il test verifica che "1+1" produca il
risultato definito dall'oracolo; è un metodo
statico della classe assert
N.B Terminologia: un test case contiene uno o più
test
30
Classi principali
• junit.framework.TestCase
– Consente l'esecuzione di più test, riportando eventuali
errori
• junit.framework.Assert
– Insieme di metodi assert
– Se la condizione di assert è falsa il test fallisce
• junit.framework.TestSuite (vedi più avanti)
– Collezione di test
– Usa l'introspezione di Java per trovare tutti i metodi che
iniziano per "test" e hanno parametri void
– Il metodo run di TestSuite esegue tutti i test
31
Esempio 2
Test di una funzione stringStrip che
elimina tutte le "a" da una stringa
public void testStringStripFunction() {
String expected = "bb"
StringStripper stripper = new
StringStripper();
assertEquals(expected,
stripper.stringStrip("aabaaaba"));
}
32
Esempio 3 (1)
// Adds up a string based on the ASCII values of its
// characters and then returns the binary representation sum
public class BinString {
public BinString() {}
public String convert(String s) {
return binarise(sum(s));
}
public int sum(String s) {
if(s=="") return 0;
if(s.length()==1) return ((int)(s.charAt(0)));
return ((int)(s.charAt(0)))+sum(s.substring(1));
}
public String binarise(int x) {
if(x==0) return "";
if(x%2==1) return "1"+binarise(x/2);
return "0"+binarise(x/2);
}
}
33
Esempio 3 (2)
import junit.framework.*;
public class BinStringTest extends TestCase {
private BinString binString;
public BinStringTest(String name) {
super(name);
}
protected void setUp() {
1
binString = new BinString();
}
public void testSumFunction() {
2
int expected = 0;
assertEquals(expected, binString.sum(""));
expected = 100;
assertEquals(expected, binString.sum("d"));
expected = 265;
assertEquals(expected, binString.sum("Add"));
}
34
Esempio 3 (3)
3
public void testBinariseFunction() {
String expected = "101";
assertEquals(expected, binString.binarise(5));
expected = "11111100";
assertEquals(expected, binString.binarise(252));
}
public void testTotalConversion() {
4
String expected = "1000001";
assertEquals(expected, binString.convert("A"));
}
}
35
Spiegazioni
1 setUp (da ridefinire) viene chiamato automaticamente
prima della valutazione di ogni test;
esiste anche tearDown da ridefinire per riportarsi
in condizioni che evitino interferenze tra test
2 test della funzione sum
3 test della funzione binarise
4 test della funzione convert
36
Ancora JUnit
• Test definiti tramite l’uso della famiglia di
ASSERTXXX()
–
–
–
–
–
assertTrue()
assertFalse()
assertEquals()
fail()
...
• È possibile eseguire una Suite di test:
– istanziare un oggetto di tipo TestSuite;
– aggiungere i test alla suite invocando il metodo
addTest(Test) sull'oggetto istanziato
37
Nuovo esempio
public class Triangolo {
private int latoA, latoB, latoC;
public Triangolo(int a, int b, int c) {
latoA = a;
latoB = b;
latoC = c; }
public boolean valido() {
if (latoA == 0 || latoB == 0 ||
latoC == 0)
return false;
if ((latoA+latoB < latoC) ||
(latoA+latoC < latoB) ||
(latoB+latoC < latoA))
return false;
return true;
}
public int perimetro() {
if (valido())
return latoA+latoB+latoC;
else
return 0; }
}
38
import junit.framework.*;
import Triangolo;
public class TestTriangolo extends TestCase {
private Triangolo t1,t2;
public TestTriangolo(String name) {
super(name); }
public void setUp() {
t1 = new Triangolo(2,4,3);
t2 = new Triangolo(2,4,8); }
public void testValido() {
assertTrue(t1.valido());
assertFalse(t2.valido()); }
...
39
...
public void testPerimetro() {
assertEquals(9,t1.perimetro());
assertEquals(0,t2.perimetro()); }
usa reflection: assume che il nome del
test sia il nome del metodo del TestCase
che va invocato
/*public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new TestTriangolo("testValido"));
suite.addTest(new TestTriangolo("testPerimetro"));
return suite;} */
public static void main(String args[]) {
junit.textui.TestRunner.run(new TestTriangolo("testValido"));
junit.textui.TestRunner.run(new TestTriangolo("testPerimetro"));
Si può eseguire un’intera suite
// junit.textui.TestRunner.run(suite());
}
}
textui è l’interfaccia testuale,
swingui è quella grafica
40
Note
/*public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new TestTriangolo("testValido"));
suite.addTest(new TestTriangolo("testPerimetro"));
return suite;} */
Il metodo suite() costruisce una TestSuite suite
Una testSuite è una classe che implementa Test. Dentro di sè tiene come
variabile privata un vettore di test
41
Un altro esempio
• public class Product
– public Product(String title, double price)
• costruttore specializzato per la classe Product. Crea un
prodotto come coppia di nome e prezzo
– public String getTitle()
• ritorna la stringa nome del prodotto
– public double getPrice()
• ritorna il prezzo del prodotto in virgola mobile su 32 bit
– public boolean equals(Object o)
• verifica che i prodotti siano uguali
• instanceof: se è possibile eseguire cast tra object o e
Product allora ritorna true altrimenti false
42
• Public class ShoppingCart
– public ShoppingCart()
–
–
–
–
–
–
• costruttore della classe ShoppingCart. Crea una istanza
del carrello come un array di items
public double getBalance()
• calcola il saldo prendendo tutti gli elementi dell’array (i
prodotti presenti nel carrello) ed eseguendo la somma di
tutti i prezzi
public void addItem(Product p)
• aggiunge un elemento (prodotto) nell’array (carrello)
public void removeItem(Product p)
• rimuove un elemento (prodotto) dall’array (carrello).
Solleva una eccezione se il carrello è vuoto
public int getItemCount()
• ritorna il numero di elementi (prodotti) nell’array
(carrello)
public void empty()
• svuota il carrello istanziando un nuovo array
public boolean isEmpty()
• ritorna true se carrello vuoto (array di dimensione zero),43
false altrimenti
• Public class ProductTest
–
–
–
–
protected void setUp()
protected void tearDown()
public ProductTest(String name)
public void testGetTitle()
• assertEquals("acer travelmate", notebook.getTitle())
– public void testGetPrice()
• assertEquals(1.100, notebook.getPrice(), 0.0)
– public void testEqualsObject()
• assertEquals(notebook2.getPrice(), notebook.getPrice(),
0.0);
• assertEquals(notebook2.getTitle(), notebook.getTitle());
• assertTrue(notebook.equals(notebook));
• assertTrue(notebook2.equals(notebook));
– public static Test suite()
44
• Public class ShoppingCartTest
–
–
–
–
public ShoppingCartTest(String name)
protected void setUp()
protected void tearDown()
public void testProductAdd()
• assertEquals(expectedBalance, _bookCart.getBalance(), 0.0)
• assertEquals(2, _bookCart.getItemCount());
– public void testEmpty()
• assertTrue(_bookCart.isEmpty())
– public void testProductRemove()
• assertEquals(0, _bookCart.getItemCount())
• assertEquals(0.0, _bookCart.getBalance(), 0.0)
– public void testProductNotFound()
• fail(“Should Raise an Exception”)
– public static Test suite()
• Public class AllTests
– public static Test suite()
45
Test di regressione
• Scenario
– programma testato con dati di test da 1 a
n senza trovare errori
– trovato errore con dato (n+1)-simo
– debugging e correzione del programma
– prosecuzione del test con dato (n+2)-simo
• Probabilità non trascurabile che la
correzione introduca errori che non lo
fanno funzionare per qualche dato da 1
a n.
46
Test di regressione (cont.)
• Consiste nel testare di nuovo il
programma, dopo una modifica, con
tutti i dati di test usati fino a quel
momento, per verificare che non si ha
una regressione
• Necessario, ma realizzabile ed
economico in pratica solo se il testing è
almeno in parte automatizzato
47
Debugging (1)
• Trovare il difetto del programma che dà origine a
comportamento erroneo rivelato dal testing
• Tecniche di debugging riconducibili a due tipi di azioni
– identificare causa effettiva usando dati di test più semplici
possibili
– localizzare porzione di codice difettoso osservando stati
intermedi della computazione
• NB: costo del debugging (spesso "contabilizzato"
sotto la voce: testing) può essere parte
preponderante del costo di sviluppo: molto
importante sviluppare il software in modo sistematico
per minimizzare sforzo speso in debugging
48
Debugging (2)
• Debugging è attivita' difficile da rendere sistematica,
efficienza dipende da persone ed è poco prevedibile,
MA occorre cercare di essere sistematici
– Identificare almeno uno stato corretto S1 e uno non corretto S2
– Cercare di capire quali stati intermedi tra S1 e S2 sono corretti e
quali no, fino a identificare uno stato corretto S’1 e uno non
corretto S’2 consecutivi
– Il difetto è nell’istruzione che separa S’1 e S’2
• Molto utile un debugger: strumento per eseguire
programmi in modo controllato:
– breakpoint,
– esecuzione passo-passo,
– visualizzazione e modifica di variabili
49
Funzionalità essenziali
• Breakpoint: permettono di interrompere l’esecuzione
in un certo punto
• Esecuzione passo passo: permette di avanzare
l’esecuzione di un passo per volta
• Esame dello stato intermedio: permette di
visualizzare il valore delle singole variabili (qui molto
utile funzione di astrazione implementata con
toString())
• Modifica dello stato: permette di modificare il valore
di una o più variabili prima di riprendere l’esecuzione
• Oggi si usano debugger “simbolici” che consentono di
operare al livello del linguaggio di programmazione
– variabile = variabile del linguaggio, non cella di memoria
– passo = istruzione del linguaggio
50
Programmazione difensiva (1)
• Un pizzico di paranoia può essere utile:
scrivere i programmi in modo che
scoprano e gestiscano ogni possibile
situazione anomala:
• procedure chiamate con parametri attuali
scorretti,
• file: devono essere aperti ma sono chiusi,
devono aprirsi e non si aprono…
• riferimenti a oggetti null, array vuoti …
• Meccanismo delle eccezioni utile aiuto
51
Programmazione difensiva (2)
• Essere scrupolosi con il test
– ricordarsi che l'obiettivo è trovare gli errori,
non essere contenti di non trovarne
– testare in particolare
• le clausole REQUIRES
• gli invarianti di rappresentazione
– codificare metodo repOK, testarlo all’inizio di ogni
operazione e prima di restituire i risultati
– può convenire dare ad altri il compito di
testare i propri programmi
52
REQUIRES o eccezioni?
//@requires x <= y
//@ensures a!=null &&
//@ (\result <==> (\exists int i; x<=i && i<=y; e==a[i] ) )
//@signals (NullPointerException e) a==null
static boolean inRange (int [] a, int x, int y, int e)
throws NullPointerException
• Se chiamata di inRange scambia secondo e
terzo parametro, implementazione diretta
potrebbe non accorgersene e restituire false
– durante il test aggiungere nel codice di inRange
controllo che x<=y e sollevare eccezione apposita
• in realtà potrebbe essere vantaggioso eliminare REQUIRES
e lasciare permanentemente eccezione
53
Controllare tutti i casi
• Può essere molto costoso, ma va fatto quando
possibile
– Esempio: ricevibili due soli comandi: "deliver" o
"examine": il codice
s = Comm.receive();
if (s.equals("deliver")) { // execute deliver}
else if (s.equals("examine")) {//execute examine}
else { // gestisci errore }
Molto meglio e poco meno efficiente di
s = Comm.receive();
if (s.equals("deliver")) { // execute deliver}
else { //execute examine }
54
Trade-offs
• Talvolta controllo è troppo costoso: se una
procedura di ricerca binaria controlla che
insieme di ricerca sia ordinato perde
efficienza
• Alternativa per controlli molto costosi: usarli
solo in fase di test e debugging (permettono
di diminuire i costi della “ricerca guasti”) e
toglierli (con attenzione e cautela,
trasformandoli in commenti) quando il
programma va in produzione
55
Scarica

Tecnologia ad Oggetti