1 Testing e Debugging. 2 Perché? Che cosa? Quando? GOAL: software con zero difetti … MA...
Transcript of 1 Testing e Debugging. 2 Perché? Che cosa? Quando? GOAL: software con zero difetti … MA...
1
Testing e Debugging
2
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!
3
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
4
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 standardizzata!
5
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
6
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
7
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
8
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?
9
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
10
Test funzionale: combinazioni proposizionali
(1)ESEMPIO• Combinare i vari casi alternativi espressi da una
specificastatic boolean isPrime (int x)
//@ensures (* \result <==> x è primo*)– Scegliere dati di test primi e non primi. Es. 5 e 8
11
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
12
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?
13
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)
14
Testing funzionale (cont.)
• Altre volte possibile e necessario usare clausola ensuresstatic boolean isPrime (int x)
//@ensures \result == true iff x prime
– Scegliere dati di test primi e non primi
15
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
16
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
17
Altri esempi
– Triangoli identificati da vertici: • tre punti allineati• due punti coincidenti• tre punti coincidenti• triangolo rettangolo• un vertice nell’origine o sugli assi • ….
18
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.
19
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
20
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
21
Specifica di IntSetpublic 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(){}
22
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
23
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
24
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)))
25
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
26
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)
27
Test di unità
JUnit
28
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, …
29
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)
}}
30
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
31
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
32
Esempio 2Test di una funzione stringStrip cheelimina tutte le "a" da una stringa
public void testStringStripFunction() {String expected = "bb"StringStripper stripper = new
StringStripper();assertEquals(expected, stripper.stringStrip("aabaaaba"));
}
33
Esempio 3 (1)// Adds up a string based on the ASCII values of its// characters and then returns the binary representation sumpublic 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);
}}
34
Esempio 3 (2)import junit.framework.*;public class BinStringTest extends TestCase {
private BinString binString;public BinStringTest(String name) {
super(name);}protected void setUp() {
binString = new BinString();}public void testSumFunction() {
int expected = 0;assertEquals(expected, binString.sum(""));expected = 100;assertEquals(expected, binString.sum("d"));expected = 265;assertEquals(expected, binString.sum("Add"));
}
1
2
35
Esempio 3 (3)
public void testBinariseFunction() {String expected = "101";assertEquals(expected, binString.binarise(5));expected = "11111100";assertEquals(expected, binString.binarise(252));
}public void testTotalConversion() {
String expected = "1000001";assertEquals(expected, binString.convert("A"));
}}
3
4
36
Spiegazioni
1setUp (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
test della funzione sum2
test della funzione binarise3
test della funzione convert 4
37
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
38
Nuovo esempiopublic 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; } }
39
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()); }
...
40
...
public void testPerimetro() { assertEquals(9,t1.perimetro()); assertEquals(0,t2.perimetro()); }
/*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"));// junit.textui.TestRunner.run(suite());
}}
textui è l’interfaccia testuale,
swingui è quella grafica
Si può eseguire un’intera suite
usa reflection: assume che il nome del test sia il nome del metodo del TestCase che va invocato
41
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
42
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
43
• 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), false altrimenti
44
• 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()
45
• 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()
46
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.
47
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
48
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
49
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
50
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
51
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
52
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
53
REQUIRES o eccezioni?//@requires x <= y//@ensures a!=null &&//@ (\result <==> (\exists int i; x<=i && i<=y; e==a[i] ) )//@signals (NullPointerException e) a==nullstatic 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
54
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 }
55
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