Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D La Corte in Rete
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Anna Rita Fasolino » 16.Automazione del testing e Analisi Mutazionale


Automazione del Testing (Testing Automation)

È 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.

A) Tecniche di Generazione dei casi di test

  • Negli approcci al testing fin qui analizzati, si è sempre considerato il task di Test Case Design come un task svolto manualmente dal tester;
    • A causa dell’elevato numero di casi di test necessari per un testing efficace, l’operazione di progettazione manuale dei casi di test può essere molto onerosa.
  • Tecniche per la generazione automatica dei casi di test possono ridurre drasticamente i costi e i tempi legati alla fase di test design;
    • Può però essere necessaria una fase di valutazione dell’efficacia dei casi di test e una fase di riduzione dei casi di test ridondanti.

Un esempio: Il Testing basato su Sessioni Utente

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.

  • In pratica, vengono installati strumenti che siano in grado di mantenere un log di tutte le interazioni che avvengono tra gli utenti dell’applicazione da testare e l’applicazione stessa (fase di Capture);
  • A partire da tali dati vengono formalizzati casi di test che replichino le interazioni “catturate” (fase di Replay)
    • In questo modo è possibile ottenere casi di test che siano rappresentativi dei reali utilizzi dell’applicazione da parte dei suoi utenti.

Problemi legati allo User Session Testing

  • Il numero di sessioni da prendere in considerazione al fine di poter avere un insieme significativo di casi di test può essere molto elevato
    • Si possono applicare tecniche di minimizzazione per la riduzione del numero di casi di test.
  • Il sistema di logging deve essere in grado di monitorare il più possibile anche gli elementi legati all’ambiente di esecuzione.
  • Per poter ottenere esecuzioni significative può essere necessario raccogliere sessioni per molto tempo
    • Per ridurre tale tempo, si possono utilizzare tecniche di Testing Mutazionale allo scopo di ottenere nuovi test case da una combinazione di quelli esistenti.

Un ulteriore Approccio: il Testing Mutazionale

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…

Testing Mutazionale

Con tale tecnica si possono ottenere Test Suites

  • più piccole (meno test cases)
  • con maggiore copertura
  • con uno sforzo minore

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

Ulteriori tecniche per la proposizione di casi di test si basano sulla valutazione analitica dei vincoli e dei valori “critici” dei dati in input

  • Domain Analysis
  • Analisi dei valori limite (boundaries)
    • Ad esempio, nel caso dell’input mese, noto che il suo insieme di validità è [1..12] (valori interi), valori limite saranno 0, 1, 12, 13

La possibilità di automatizzare tali tecniche dipende dalla disponibilità di strumenti per l’analisi automatica di specifiche, codice, o altro…

B) Preparazione ed Esecuzione del Test

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:

  • JUnit (Java)
  • CppUnit (C++)
  • csUnit (C#)
  • NUnit (.NET framework)
  • HttpUnit e HtmlUnit (Web Application)

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.

Aspetti fondamentali dello Unit Testing

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.

  • 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:
      • Quali precondizione devono essere poste prima di poter eseguire un caso di test;
      • Quali postcondizioni sui valori dello stato degli oggetti devono verificarsi.
  • Svantaggi:
    • Lo sviluppatore tende a difendere il suo lavoro … troverà meno errori di quanto possa fare un tester!

Vantaggi dell’esecuzione automatica del Test

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:

  • Tempo risparmiato (nell’esecuzione dei test);
  • Affidabilità dei test (non c’é rischio di errore umano nell’esecuzione dei test);
  • Riuso (parziale) dei test a seguito di modifiche nella classe.

Unit Testing ed esecuzione automatica dei test

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.

  • Problemi
    • Tale codice verrà distribuito anche nel prodotto finale, appesantendolo;
    • La classe potrebbe già avere un main;
    • Come strutturare i test case? Come eseguirli separatamente?

Per ovviare a questi problemi, è necessario un approccio sistematico:

  • Che separi il codice di test da quello della classe;
  • Che supporti la strutturazione dei casi di test in test suite;
  • Che fornisca un output separato dall’output dovuto all’esecuzione della classe.

L’approccio di JUnit

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.

Il Modello delle classi del framework JUnit.

Il Modello delle classi del framework JUnit.


Le Componenti del Framework JUnit

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.

Plug-in di Eclipse per JUnit

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:

  • Creare classi contenenti test cases;
  • Automatizzare l’esecuzione di tutti i test cases;
  • Mostrare i risultati dell’esecuzione dei casi di test;
  • Organizzare i test cases in test suites.

Verifica dell’esito del Test mediante asserzioni

ALCUNE ASSERZIONI: (Elenco completo)

  • static void assertEquals(boolean expected, boolean actual)
    • Asserts that two booleans are equal.
  • static void assertEquals(int expected, int actual)
    • Asserts that two ints are equal.
  • static void assertEquals(java.lang.String expected, java.lang.String actual)
    • Asserts that two Strings are equal.
  • static void assertFalse(boolean condition)
    • Asserts that a condition is false.
  • static void assertTrue(boolean condition)
    • Asserts that a condition is true.
  • static void assertNull(java.lang.Object object)
    • Asserts that an object is null.
  • static void fail()
    • Fails a test with no message.
  • static void fail(java.lang.String message)
    • Fails a test with the given message.

Alcuni dettagli per l’uso di JUnit

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.

Limiti di JUnit

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!
  • JUnit supporta unicamente il testing, non il debugging
    • L’utilizzo di molte asserzioni può, però, portare ad indicazioni dettagliate sulle ragioni del successo del test case.

Limiti di JUnit (segue)

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.

Altre applicazioni di Testing Automation

  • La valutazione dell’efficacia di Test Suite;
  • Generazione di moduli Driver e Stub.

    C) La Valutazione dell’Efficacia di una Test Suite

    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.

    Sviluppo di Strumenti automatici a supporto

    É 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.

    D) Ausili per il Testing d’integrazione

    • Il testing é applicato ad un aggregato di due o più unità di un sistema software;
    • l’obiettivo é quello di rilevare errori nelle interazioni fra le unità e nelle funzioni che l’aggregato deve assolvere;
    • non é compito dei programmatori che hanno prodotto le unità componenti;
    • le unità da integrare sono selezionabili in base a criteri funzionali ricavabili dal progetto (architettura del sistema);
    • partendo da una architettura organizzata gerarchicamente, le integrazioni possono essere realizzate con approccio top-down o bottom-up o misto.

    Strategie per il testing di integrazione

    Tre possibili strategie di Testing di Integrazione per Architetture Software a livelli.

    Tre possibili strategie di Testing di Integrazione per Architetture Software a livelli.


    Esecuzione del test

    Costruzione di Moduli guida (driver)
    invocano l’unità sotto test, inviandole opportuni valori, relativi al test case.

    Moduli fittizi (stub)

    • sono invocati dall’unità sotto test;
    • emulano il funzionamento della funzione chiamata rispetto al caso di test richiesto (tenendo conto delle specifiche della funzione chiamata)
      • Quando la funzione chiamata viene realizzata e testata, si sostituisce lo stub con la funzione stessa.

    Driver

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

    Stub

    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.

    Testing di regressione e Ripple effect

    • Dopo un intervento di manutenzione, è probabile che la modifica effettuata influisca sul resto del sistema, generando nuovi difetti (è il cosiddetto ripple effect)
      • Chi corregge potrebbe non avere una adeguata conoscenza di tutto il sistema e delle sue connessioni;
      • Il sistema può regredire (“invecchiare”) verso uno stato più difettoso.
    • Occorre eseguire il Testing di Regressione
      • È però molto costoso ripetere tutti i singoli test, a seguito di un’unica modifica fatta;
      • Sarebbe utile poter automatizzare tale attività.

    Testing di regressione

    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?

    Impact Analysis e grafo delle dipendenze

    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

    • tutti i moduli m’ che da essi dipendono (per i quali esista un arco m’→m) sono sicuramente impattati dalla modifica di m;
    • Tutti I moduli m” che dipendono da uno qualunque dei moduli m’ saranno a loro volta impattati, e così via.

    Grafo delle dipendenze ed Analisi dell’Impatto

    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]

    Un esempio di grafo delle dipendenze.

    Un esempio di grafo delle dipendenze.


    Testing di regressione

    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.

    Analisi Mutazionale

    I criteri di copertura finora presentati cercano di coprire:

    • Le funzionalità dichiarate dell’applicazione (testing funzionale);
    • Gli elementi della struttura dell’applicazione (testing strutturale).

    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?

    Analisi Mutazionale (segue)

    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.


    Analisi Mutazionale (segue)

    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!

    Analisi Mutazionale (segue)

    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.

    Problemi dell’analisi mutazionale

    Difficoltà nella modellazione dei difetti di un sistema software

    • Di solito vengono proposti degli operatori di mutazione
      • Basandosi sull’esperienza generica di chi propone il testing mutazionale riguardo le possibili cause di errore;
      • Basandosi su di un’analisi statistica dei difetti rilevati in altri 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:

    • Operatore che sostituisce un’operazione aritmetica con un’altra
    • Operatore che sostituisce un operatore relazionale con un altro
    • Operatore che inverte la posizione di due righe di programma, etc.

    Problemi dell’analisi mutazionale (segue)

    • Il numero di possibili difetti è estremamente elevato, già per un piccolo frammento di software
      • Il numero di mutanti generati da un piccolo insieme di operatori è estremamente elevato
        • Per ogni mutante dovrebbero essere eseguiti tutti i casi di test di una test suite
          • L’approccio è possibile solo in presenza di un ambiente per la testing automation, e anche in questo caso è molto oneroso;
          • Spesso si decide di applicare gli operatori di mutazione solo “a campione”.

    Problemi dell’analisi mutazionale (segue)

    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.

    Vantaggi

    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.

    • Contenuti protetti da Creative Commons
    • Feed RSS
    • Condividi su FriendFeed
    • Condividi su Facebook
    • Segnala su Twitter
    • Condividi su LinkedIn
    Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion

    Fatal error: Call to undefined function federicaDebug() in /usr/local/apache/htdocs/html/footer.php on line 93