È l’insieme delle tecniche e delle tecnologie che consentono di automatizzare (anche parzialmente) alcune attività del Processo di Testing.
Alcune aree di intervento:
A) Generazione dei Casi di Test;
B) Preparazione ed Esecuzione del Test;
C) Valutazione dell’efficacia di Test Suite.
Tecnica per la generazione automatica di casi di test per il testing black box partendo dall’analisi delle sessioni utente (User Session), ovvero delle sequenze dei valori di input immessi e di output ottenuti in utilizzi reali del software.
Il Testing Mutazionale è una tecnica per la generazione automatica di casi di test.
A partire da un sottoinsieme di casi di test, si applicano alcuni operatori di mutazione che vadano a modificare/incrociare i dati dei test case esistenti, in modo da ottenere nuovi test case.
Es. Si cambia il segno degli input, si raddoppiano i valori di input, si combinano sequenze di input in nuove sequenze, etc…
Con tale tecnica si possono ottenere Test Suites
rispetto a quelle ottenute semplicemente collezionando sessioni utente.
Bisogna però eliminare tutti i test cases che risultano non applicabili.
Questa tecnica è spesso utilizzata per il testing di interfacce o di protocolli.
Ulteriori tecniche per la proposizione di casi di test si basano sulla valutazione analitica dei vincoli e dei valori “critici” dei dati in input
La possibilità di automatizzare tali tecniche dipende dalla disponibilità di strumenti per l’analisi automatica di specifiche, codice, o altro…
Una soluzione per l’automazione del testing Black Box consiste nell’utilizzare appositi framework di supporto all’esecuzione dei casi di test.
Tra questi framework, molto noti sono quelli della famiglia XUnit, come ad esempio:
Tali framework sono nati nell’ambito della eXtreme Programming per automatizzare il testing di unità, ma possono essere generalizzati anche alle problematiche di testing black box.
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.
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:
Nel caso di Unit Testing di una classe, una tecnica per l’esecuzione automatica del test richiede di scrivere in ogni classe un metodo main capace di testare i comportamenti della classe.
Per ovviare a questi problemi, è necessario un approccio sistematico:
JUnit [sito, Progetto sourceforge] é un framework che permette la scrittura di test in maniera ripetibile.
Fu sviluppato originariamente da Erich Gamma and Kent Beck, nell’ambito degli strumenti a supporto dell’eXtreme Programming.
Test: É l’interfaccia che tutti I tipi di classi di test devono implementare. Attualmente il framework JUnit contiene solo due classi di test, TestCase e TestSuite.
TestCase: É la classe fondamentale che deve essere estesa per scrivere nuovi test. Un TestCase concreto (cioè una classe che estende TestCase) ha sia metodi che implementano i veri e propri test, sia i metodi opzionali di setUp (che settano le pre-condizioni necessarie prima dell’esecuzione di un Test) e tearDown (che eseguono le operazioni necessarie ad azzerare gli effetti prodotti dal test).
TestSuite: classe che implementa l’interfaccia di Test. Il suo scopo è di raggruppare Tests di TestCases, o TestSuites.
Assert: superclasse di TestCase che fornisce tutti I metodi assert disponibili nel framework per verificare l’esito del test.
TestFailure: classe che incapsula l’errore o la failure che si verificano durante l’esecuzione del test. Essa tiene traccia del Test fallito e dell’eccezione responsabile dell’errore o della failure.
TestResult: contiene tutti i risultati dell’esecuzione dei test.
TestListener: interfaccia che viene implementata da ogni classe che vuole tenere traccia del progresso dell’esecuzione di un test. Dichiara I metodi per notificare l’inizio e la fine di ciascun test, e l’occorrenza di errori e failures.
Alcuni ambienti di sviluppo software mettono a disposizione dei Plug-ins che semplificano il processo di scrittura ed esecuzione dei test JUnit su classi Java.
Ad esempio, Eclipse é dotato di un plug-in, di pubblico dominio, che supporta tutte le operazioni legate al testing di unità con JUnit. In particolare, esso fornisce dei wizard per:
ALCUNE ASSERZIONI: (Elenco completo)
Precondizioni → metodo setUp (se comuni a più casi di test), altrimenti all’inizio del metodo di test.
Input → si inseriscono sotto forma di assegnazioni all’inizio del codice del test case, oppure si passano come parametri della chiamata della funzione da testare.
Output → si valutano tramite asserzioni sui valori restituiti dalla funzione testata.
Postcondizioni → si valutano tramite asserzioni su oggetti in qualche modo coinvolti nell’esecuzione del caso di test.
Ripristino delle condizioni iniziali → metodo tearDown (se comuni a più casi di test) oppure al termine del metodo di test. Sono necessari per poter ripristinare lo stato del sistema al fine di automatizzare l’esecuzione in sequenza dei 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 …
Si é data 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!
La generazione del codice delle classi di test è automatizzabile.
Una misura di efficacia è data dal grado di Copertura raggiunto dalla test suite.
In generale, dato un criterio di copertura, è abbastanza semplice valutare la sua effettiva copertura;
mentre non è per nulla semplice generare dei casi di test che siano in grado di garantire tale copertura.
Una possibile sinergia fra White Box e Black Box Testing:
La valutazione della copertura con criteri white box è un metodo per valutare, in sede sperimentale, in maniera indiretta, l’efficacia delle tecniche di generazione di test black box.
É possibile sviluppare programmi che inseriscano automaticamente “sonde” all’interno di un programma in un determinato linguaggio di programmazione, allo scopo di valutare l’efficacia di una test suite rispetto a criteri di copertura strutturali.
Ad esempio, se venisse inserita una sonda in corrispondenza di ogni linea di codice eseguibile e che produca in output la linea di codice visitata, allora si potrebbe valutare, a seguito dell’esecuzione di una certa test suite, l’insieme delle linee di codice visitate.
Costruzione di Moduli guida (driver)
invocano l’unità sotto test, inviandole opportuni valori, relativi al test case.
Moduli fittizi (stub)
Tramite un framework di Testing Automation come JUnit è possibile realizzare driver e stub necessari per testare un modulo non terminale.
Le classi di test per JUnit rappresentano dei driver quando si riferiscono ad una classe non accessibile dall’esterno.
Un driver deve avere la visibilità dei componenti da testare (o quanto meno dell’interfaccia che vogliamo esercitare).
Uno stub è una funzione fittizia la cui correttezza è vera per ipotesi.
Esempio, se stiamo testando una funzione prod_scal(v1,v2) che richiama una funzione prodotto(a,b) ma non abbiamo ancora realizzato tale funzione.
Nel metodo driver scriviamo il codice per eseguire alcuni casi di test
Ad esempio chiamiamo prod_scal([2,4],[4,7])
Il metodo stub potrà essere scritto così:
int prodotto (int a, int b){
if (a==2 && b==4) return 8;
if (a==4 && b==7) return 28;
}
La correttezza di questo metodo stub è data per ipotesi.
Ovviamente per poter impostare tale testing, bisognerà avere precise informazioni sul comportamento interno richiesto al modulo da testare.
Si applica in seguito ad un intervento di manutenzione su di un software esistente, per il quale esiste già un piano di test.
Un problema: quali test devono essere riprogettati? E quali test possono essere riusati?
Sicuramente devono essere riprogettati tutti i casi di test relativi alla nuova funzionalità implementata (o alla funzionalità modificata).
Quali altri test dovranno essere rieseguiti?
Per determinare quali test preesistenti devono essere rieseguiti, occorre valutare quali altre funzionalità potrebbero essere state influenzate dalla modifica realizzata, ossia eseguire l’Impact Analysis:
Quale sarà stato l’impatto della modifica sul sistema?
L’analisi di impatto è la disciplina che permette di conoscere, data una modifica, quali parti del software possono esserne influenzate (e quindi devono essere ri-testate).
Una tecnica semplice per la valutazione dell’impatto è basata sul Grafo delle Dipendenze.
Un grafo delle dipendenze ha tanti nodi quanti sono I moduli (classi/funzioni), e un arco per ogni associazione/dependency tra i moduli.
Nei sistemi object oriented il grafo delle dipendenze può essere banalmente ricavato dal diagramma di progetto di dettaglio.
Data una modifica su di un modulo m
Se in fase di progettazione del software, tutte le dipendenze fra artifatti fossero registrate esplicitamente, si potrebbe facilmente eseguire tale Analisi di Impatto.
[A.R. Fasolino, G. Visaggio "Improving Software Comprehension through an Automated Dependency Tracer", IEEE Workshop on Program Comprehension, 1999]
I casi di test relativi ai moduli impattati (oppure tutti i moduli, nel caso in cui non sia stato possibile effettuare impact analysis) devono essere rieseguiti.
L’oracolo del testing di regressione è fornito dall’esito dei test che si otteneva prima di eseguire la modifica.
Il testing di regressione si presta, quindi, facilmente ad essere automatizzato.
Qualora invece non fosse automatizzato, il testing di regressione diventerebbe una pratica molto onerosa e renderebbe molto costosa l’adozione di un ciclo di vita evolutivo, come ad esempio quelli di sviluppo agile che fanno uso di tecniche di eXtreme Programming.
I criteri di copertura finora presentati cercano di coprire:
Un criterio di copertura “ideale” sarebbe quello di riuscire a coprire tutti gli elementi difettosi presenti nell’applicazione.
Un Problema:
Come è possibile valutare la capacità potenziale di una data test suite di rilevare difetti?
Il primo passo consiste nell’immaginare quali possano essere i possibili errori (fault model).
È necessario proporre un modello degli errori e dei corrispondenti operatori di mutazione.
Un operatore di mutazione introduce in un programma, supposto corretto, un difetto (realizzando un’operazione di fault injection), trasformando il programma originale in un mutante.
I difetti (fault) sono inseriti automaticamente nei programmi, ottenendo dei mutanti.
Su ogni mutante generato si vanno ad eseguire i test case progettati per l’applicazione originale.
L’oracolo per l’esecuzione di tali test è dato dall’output che veniva generato dal programma originale
Se l’esito del test è positivo (l’output del mutante differisce da quello del programma originale), allora si dice che il mutante è stato ucciso (killed).
Altrimenti, il mutante non è stato rivelato dal test, ed è sopravvissuto.
Più mutanti sono uccisi, maggiore fiducia si può avere nella capacità della Test Suite di scoprire difetti!
L’efficacia di una test suite si può misurare come
TER = # killed Mutants / # Mutants
In conclusione:
Una test suite che riesca a rivelare il maggior numero possibile di mutanti è da considerarsi più promettente nella rivelazione di potenziali difetti nell’applicazione.
Difficoltà nella modellazione dei difetti di un sistema software
Gli operatori proposti sono di solito estremamente generici, per poter essere applicabili ad ogni software, indipendentemente dal linguaggio adottato e dalle caratteristiche della singola applicazione.
Esempi:
Altri problemi:
Non tutti i difetti di un sistema software sono legati ad errori nel codice sorgente.
Si pensi a difetti del tipo di: mancanza di un requisito, scorretta interpretazione di un requisito, etc.
Alcuni difetti portano ad errori sintattici e sono già rivelati dal compilatore.
Non è possibile proporre operatori generali che riproducano gli errori semantici.
L’analisi mutazionale è una tecnica completamente automatica, che può essere eseguita in batch senza l’assistenza del tester.
Occorre però disporre di strumenti automatici per la generazione dei mutanti, l’esecuzione dei test, la valutazione dei risultati (…)
Si rivela utile come banco di prova per il confronto in ambiente sperimentale tra diverse test suite, per valutare quale sia in grado di rilevare più difetti.
2. Ciclo di Vita e Processi Software
3. Processi per lo sviluppo rapido del software
4. Sviluppo Agile del Software
5. Test Driven Development (TDD)
7. Component Based Software Engineering (CBSE): Generalità
8. Component Based Software Engineering (CBSE): Il processo di svi...
9. Ingegneria del Software orientato ai Servizi
10. Ingegnerizzazione dei Servizi
11. I Processi di Manutenzione del Software
12. Reengineering, Refactoring e Reverse Engineering del Software
13. Verifica e Convalida del Software. Richiami e concetti di base ...
14. Tecniche di Testing Dinamico
15. Testing di Sistemi Object Oriented
16. Automazione del testing e Analisi Mutazionale
17. Tecniche di Analisi Statica del codice e il Debugging
18. Stima dei costi nei progetti Software
19. Il Modello COCOMO per la stima dei costi Software – La gestio...
20. Gestione e Miglioramento dei Processi di Produzione del Softwar...
21. La Valutazione della Qualità dei Processi Software – Il Capa...