Si esercita il sistema immettendo input e osservando i valori degli output.
Non conosciamo (oppure non teniamo in conto):
Viceversa, conosciamo e utilizziamo per la progettazione dei casi di test:
Obiettivi principali del testing black box possono essere:
Le difficoltà principali sono legate a:
La soluzione più spesso adottata è quella di
Occorre dividere i possibili input in gruppi i cui elementi si ritiene che saranno trattati similarmente dal processo elaborativo.
Questi gruppi saranno chiamati classi di equivalenza.
Una possibile suddivisione è quella in cui classe di equivalenza rappresenta un insieme di stati validi o non validi per una condizione sulle variabili d’ingresso.
Chi esegue il testing eseguirà almeno un test per ogni classe di equivalenza.
Criterio di copertura delle classi di equivalenza.
Se la condizione sulle variabili d’ingresso specifica:
Inoltre, in ogni caso:
Una classe non valida per valore∉tipo
Ogni classe di equivalenza deve essere coperta da almeno un caso di test.
Un caso di test per ogni classe non valida.
Ciascun caso di test per le classi valide dovrebbe comprendere il maggior numero di classi valide ancora scoperte.
Una condizione di validità per un input password è che la password sia una stringa alfanumerica di lunghezza compresa fra 6 e 10 caratteri.
Una classe valida CV1 è quella composta dalle stringhe di lunghezza fra 6 e 10 caratteri.
Due classi non valide sono:
Un programma C++ riceve in input una data, composta di giorno (numerico), mese (stringa che può valere gennaio … dicembre), anno (numerico, compreso tra 1900 e 2000) e restituisce il giorno della settimana corrispondente (1= lunedì, 2= martedì … 7=domenica).
Selezionare i casi di test mediante partizione in classi di equivalenza.
Per esercizio, provare ad eseguire i test previsti sulla funzione riportata di lato.
Il codice viene riportato per comodità, ma il testing viene progettato senza analizzare il codice sorgente.
Condizioni d’ingresso:
Classi di equivalenza:
Condizioni di ingresso:
Il mese deve essere nell’insieme M=(gennaio, febbraio, marzo, aprile, maggio, giugno, luglio, agosto, settembre, ottobre, novembre, dicembre).
Classi di equivalenza
Valide
CE5: MESE ∈ M
Non valida
CE6: MESE ∉ M
Condizioni di ingresso:
Deve essere compreso tra 1900 e 2000
Classi di equivalenza
Valida
CE7: 1900<= ANNO<=2000
Non valide
CE8: ANNO< 1900
CE9: ANNO> 2000
CE10: ANNO non è un numero intero
Ogni TC riesce a coprire almeno una classe di equivalenza non coperta da alcuno dei precedenti.
La Test Suite comprendente TC1…TC8 copre tutte le classi di equivalenza (ma non tutte le possibili combinazioni …)
Ad esempio, non viene testata la risposta del sistema ad una data di nascita come il 30 febbraio!
Per valutare la qualità della test suite bisognerebbe valutare contemporaneamente:
Nel nostro caso, proporre casi di test in grado di sollecitare tutte le combinazioni ammissibili degli input farebbe aumentare probabilmente l’efficacia della test suite riducendo l’efficienza.
Una Test Suite più efficiente potrebbe essere questa a lato.
Tutte le classi di equivalenza sono coperte ma …
È molto più difficile individuare gli errori.
Ad esempio in TC2 il sistema potrebbe rispondere con un’eccezione perchè il giorno é <1, senza valutare il mese e l’anno!
A volte non é possibile determinare staticamente le classi di equivalenza. Esempio: un sistema accetta password di tipo stringa. Classi di equivalenza possono essere:
Nella descrizione dei casi di test bisogna quindi tener conto di precondizioni: vedi figura.
L’appartenenza ad una classe di equivalenza dipende quindi dallo stato dell’applicazione.
Il testing a livello di unità dei comportamenti di una classe dovrebbe essere progettato ed eseguito dallo sviluppatore della classe, contemporaneamente allo sviluppo stesso della classe.
Di questa opinione sono in particolare Erich Gamma e Kent Beck, meglio conosciuti come gli autori dei Design Pattern e dell’eXtreme Programming (che verrà presentato nella lezione dedicata ai cicli di vita).
Vantaggi:
Lo sviluppatore conosce esattamente le responsabilità della classe che ha sviluppato e I risultati che da essa si attende.
Lo sviluppatore conosce esattamente come si accede alla classe, ad esempio:
Svantaggi:
Lo sviluppatore tende a difendere il suo lavoro … troverà meno errori di quanto possa fare un tester!
Se la progettazione dei casi di test é un lavoro duro e difficile, l’esecuzione dei casi di test é un lavoro noioso e gramo!
L’automatizzazione dell’esecuzione dei casi di test porta innumerevoli vantaggi:
Scrivere un metodo di prova (“main”) in ogni classe contenente del codice in grado di testare I suoi comportamenti.
Problemi
Per tutti questi motivi, cerchiamo un approccio sistematico:
La soluzione alle problematiche precedenti é data dai framework della famiglia X-Unit:
JUnit é un framework (in pratica consiste di un archivio .jar contenente una collezione di classi) che permette la scrittura di test in maniera ripetibile.
Una classe di test, contiene:
Inizializzazione precondizioni
Limitatamente alle precondizioni tipiche del singolo caso di test, le altre potrebbero essere nel setup.
Inserimento valori di input
Tramite chiamate a metodi set oppure tramite assegnazione di valori ad attributi pubblici.
Codice di test
Esecuzione del metodo da testare con gli eventuali parametri relativi a quel caso di test.
Valutazione delle asserzioni
Controllo di espressioni booleani (asserzioni) che devono risultare vere se il test dà esito positivo, ovvero se i dati di uscita e/o le postcondizioni riscontrati sono diversi da quelli attesi.
Il metodo setUp() può essere completato, accodando tutte quelle operazioni da effettuare preliminarmente a qualsiasi test che sarà descritto in questa classe;
Il metodo tearDown() conterrà il codice relativo a tutte le operazioni comuni da effettuare dopo l’esecuzione di ogni test di questa classe (ad esempio per ripristinare lo stato della classe prima dell’esecuzione del prossimo test.
Scriviamo un metodo testSomma che rappresenti un caso di test per il metodo Somma;
Siccome il metodo appartiene ad una classe calcolatriceTest nello stesso package della classe da testare, il metodo test può istanziare oggetti della classe ed accedere ai suoi metodi.
public void testSomma() {
calcolatrice c=new calcolatrice();
int a=5,b=7;
int s=c.somma(a,b);
assertEquals("Somma non corretta!",12,s);
}
Il metodo assertEquals verifica se s (valore ottenuto dall’esecuzione del metodo somma é uguale a 12 (valore atteso); in caso contrario conta questo fatto come una failure e genera il messaggio di errore indicato
assertEquals("Somma non corretta!",12,s);
public void testDivisione(){
calcolatrice c=new calcolatrice();
int a=15,b=2;
double s=c.divisione(a,b);
assertTrue(s==7.5);
}
public double divisione (int a, int b)
{return a/b;}
In realtà il metodo divisione restituisce la divisione intera …
Grazie a JUnit possiamo prontamente trovare il rigo con l’asserzione errata e avviare il debugging …
Quando pensiamo di aver corretto l’errore rieseguiamo il test …
Se una classe é in associazione/dependency con altre e JUnit rileva l’errore, esso potrebbe dipendere dall’altra classe (se non é stata testata adeguatamente) oppure dall’integrazione …
JUnit é uno strumento che é in grado di risolvere SOLO le problematiche relative al testing di unità … dà solo qualche utile indicazione rispetto al testing di integrazione!
Si é dato per scontata la correttezza del codice delle classi di test … se avessimo voluto esserne sicuri al massimo avremmo potuto fare il test di unità delle classi di test stesse (paradossale!)
Il codice delle classi di test é comunque estremamente lineare e ripetitivo: la possibilità di sbagliare é ridotta!
1. Introduzione
4. Casi d'uso
6. Class Diagram – parte prima
7. Class diagram – parte seconda
8. Class diagram – parte terza
9. Modellazione architetturale
10. Sequence Diagram
14. Progettazione Architetturale
15. Design Patterns – Parte prima
16. Design Patterns – Parte seconda
17. Progettazione dell'interfaccia utente
Sommerville, Ingegneria del Software, 8° edizione, Capitoli 22-23-24.