Scuola Politecnica e delle Scienze di Base Corso di Laurea in Ingegneria Informatica Elaborato finale in Ingegneria del Software Metodi e strumenti per il model-based testing. Anno Accademico 2013/2014 Candidato: Andrea Capuano Matr. N46/001274 Indice Indice .................................................................................................................................................. III Introduzione ......................................................................................................................................... 4 Capitolo 1: Model based testing process.............................................................................................. 7 1.1 Il processo MBT. ................................................................................................................... 7 1.2 Pro e contro dell’approccio Model Based. ............................................................................ 9 1.3 Tassonomia.......................................................................................................................... 11 1.4 Tools per il Model Based Testing. ...................................................................................... 12 Capitolo 2: Costruzione del modello. ................................................................................................ 13 2.1 Testing con macchina a stati finiti....................................................................................... 13 2.2 Testing con Notazioni state-based (Pre/Post condizioni). ................................................... 15 2.3 Generazione dei test da modelli UML. ............................................................................... 19 Capitolo 3: Caso di studio: classe File. .............................................................................................. 25 3.1 Creazione del modello. ........................................................................................................ 25 3.2 Implementazione del modello della FSM in Java. .............................................................. 27 3.3 Implementazione delle operazioni con RandomAccessFile. ............................................... 30 Conclusioni ........................................................................................................................................ 32 Bibliografia ........................................................................................................................................ 33 Introduzione Se si traccia la funzione costo di correzione di un bug in funzione del momento in cui lo si scopre, notiamo che ha un andamento di tipo esponenziale[1] (Fig. 1). Questo ci porta a voler trovare un eventuale bug il prima possibile per evitare gli elevati costi del bug-fixing tardivo. Si cercano quindi nuove tecniche di testing affinché eventuali malfunzionamenti siano trovati in fasi precedenti al rilascio del software. Figura 1 - Andamento esponenziale della curva costo di correzione In questa ottica si colloca il model-based testing, una tecnica ancora in fase di evoluzione che consente di generare, in maniera automatica, casi di test da modelli del sistema. Una delle più grandi sfide durante la costruzione di un modello è quella di trovare un giusto 4 equilibrio tra schematicità e attenzione per i dettagli, infatti si vuole che siano preservate le caratteristiche di ciò che si sta descrivendo e al tempo stesso che non si comprometta eccessivamente la lettura e comprensione del modello. Finita la fase di design si passa alla generazione dei test, uno dei punti di forza dell’approccio model-based, infatti sono numerosi gli strumenti, sia open source che commerciali, che consentono di produrre test in maniera automatica. I test prodotti sono “astratti” ovvero è necessario trasformarli, attraverso templates e tabelle di traduzione, in test eseguibili. L’approccio model-based ha come suo punto di forza quello di essere fortemente descrittivo tuttavia introduce nuove problematiche, tra le quali il ruolo centrale del modello che, se costruito in maniera scorretta, può portare alla generazione di casi di test incoerenti con il SUT (System Under Test). Figura 2 - Differenti tipologie di testing La figura 2 esprime la posizione del model-based testing in funzione di tre assi: La scala del sistema, le caratteristiche che si stanno testando e da dove sono generati i test (requisiti o codice). Si noti che si ha la possibilità di effettuare testing sia su piccola che grande scala, quindi l’approccio MBT è utilizzabile a partire dal testing di unità fino ad arrivare all’intero sistema software. 5 Il model based testing risulta particolarmente efficiente nel realizzare testing funzionale, in quest’ottica è importante il ruolo dell’oracolo che ci consente di verificare che l’output atteso, dato un particolare input, coincida con quello effettivo. Infine, poiché la costruzione del modello è basata sui requisiti del sistema, il test è di tipo blackbox. 6 Capitolo 1: Model based testing process. 1.1 Il processo MBT. La creazione di un modello astratto del sistema ci consente di generare i casi di test in maniera automatica. In figura 3 si schematizza l’intero processo in cinque macro fasi: 1. Creazione del modello. 2. Generazione dei test astratti. 3. Concretizzazione dei test astratti in eseguibili. 4. Esecuzione dei test. 5. Analisi dei risultati. La fase di stesura del modello è il nucleo dell’approccio model-based. La procedura non è automatizzabile, tuttavia esistono degli strumenti di annotazione che consentono di verificare che il comportamento del modello sia compatibile con quello desiderato. Figura 3- Processo model-based 7 Tra le problematiche da affrontare ci sono: Decidere il livello di astrazione del sistema. Scegliere una notazione per il modello. Modellare il sistema in base alle operazioni che deve svolgere e i dati che deve gestire. Validare il modello. Per quanto riguarda il livello di astrazione bisogna ricordare che il modello costruito è finalizzato al testing, dunque non si vuole una descrizione interna e minuziosa del sistema ma un’astrazione di quest’ ultimo che ci consenta di generare casi di test. La scelta della notazione è dipendente dal tipo di sistema che si sta testando. Tra le più utilizzate ci sono le state-based notations e le transition-based notations. La prima utilizza pre-condizioni e post-condizioni per verificare la consistenza del modello mentre la seconda sfrutta diagrammi come la macchina a stati finiti o le statecharts (tra cui le UML State Machines). La seconda fase è quella di generare dei casi di test dal modello. Bisogna effettuare una importante scelta progettuale ovvero la selezione dei test. Idealmente si possono generare infiniti test, tuttavia per rispettare le scadenze temporali ed evitare eccessivi costi è necessario generare un numero di casi di test non infinito, che consentano però di testare a fondo il sistema. Affinché ciò sia possibile si utilizzano dei criteri di copertura di cui si discuterà nel successivo capitolo. Molti strumenti automatici generano anche la matrice di tracciabilità dei requisiti, che collega i requisiti funzionali ai casi di test generati, inoltre ci consente di scoprire il numero di test effettuati su un particolare requisito. È un modo particolarmente veloce per capire se ci sono requisiti che sono stati trascurati e dunque richiedono testing ulteriore. Per concretizzare un test astratto in un test eseguibile ci sono tre approcci: Scrivere manualmente il codice che colleghi i test astratti al SUT (System Under Test). Uso di tools automatici che sfruttano templates e tecniche di mapping per effettuare la trasformazione. Approccio misto. L’approccio caso di test astratto / caso di test concreto ha il vantaggio di rendere i test generati indipendenti sia dal linguaggio di programmazione utilizzato sia dall’ambiente di testing. 8 La fase di verifica presenta un ulteriore problema nell’approccio MBT, infatti il fallimento di un caso di test può essere scatenato da un errore nel codice del sistema software in analisi, oppure da un problema dovuto all’errata generazione del caso di test. Nell’ultimo caso scoprire la causa dell’errore può essere complesso, in quanto questa va ricercata o nel modello astratto del sistema o nel codice di trasformazione da test astratto a concreto. 1.2 Pro e contro dell’approccio Model Based. L’esperienza condotta dalla Microsoft nell’utilizzo quotidiano della tecnica model-based è stata estremamente positiva. Infatti gli errori trovati, rispetto ai casi di test creati manualmente, sono circa dieci volte in più[2]. Tuttavia è importante notare che l’efficienza dell’approccio MBT è sempre relazionata alla correttezza del modello e al particolare sistema su cui si sta lavorando, dunque questi dati statistici non possono essere prova certa del fatto che il testing model based sia più efficiente, in termini di errori scoperti, rispetto al testing manuale. La manutenzione e l’aggiornamento dei casi di test diventano più semplice con l’introduzione del modello[3]. Infatti un cambiamento del software si traduce in semplici modifiche correttive del modello, poi saranno i tools automatici a generare casi di test aggiornati. Inoltre ci si avvicina verso il paradigma “Design more and code less”, ovvero si pone più attenzione nella fase di progettazione del software piuttosto che la scrittura di codice. Un altro punto a favore dell’approccio MBT è la facile tracciabilità, ovvero la semplicità con cui riusciamo a relazionare un caso di test al modello e ai requisiti del sistema. Come già discusso in precedenza ci sono tools che possono generare in maniera automatica la matrice di tracciabilità che ci consente di osservare la relazione caso di test – requisito. Tra gli svantaggi del Model Based Testing ci sono la curva d’apprendimento più ostica rispetto al testing tradizionale. Uno studio effettuato dalla Intrasoft International ha evidenziato che l’uso dei tools basati su MBT risulta più efficiente a lungo termine a discapito di una maggiore durata della fase di testing. Infatti molto tempo era impiegato per imparare l’utilizzo degli strumenti necessari per avvicinarsi alla tecnica Model-based[4]. 9 Un’altra limitazione del MBT è il poco utilizzo in testing di tipo non-funzionale[5]. Infatti sono ancora pochi gli studi che utilizzano questa tecnica per testing di robustezza, performance o usabilità. Si schematizzano in tabella i pro e contro del model-based testing: Pro Contro Forte espressività del modello. Difficoltà nell’apprendimento della tecnica. Buona efficienza nella detection di errori. Limitato a testing di tipo funzionale. Tracciabilità dei requisiti. Il fallimento di un caso di test può avere molteplici cause non sempre facili da individuare. Design more, code less. Numero di casi di test illimitato. 10 1.3 Tassonomia. Figura 4- Tassonomia degli approcci model based[6] Affinché sia possibile delineare una tassonomia degli approcci model based si utilizzano le seguenti classificazioni: Scope: Il modello può rappresentare unicamente gli input oppure input-output. Il primo approccio è più semplice, ma ha il forte svantaggio di non poter far sì che i test generati possano agire da oracolo. Caratteristiche del modello: E’ una classificazione intrinseca del modello, dovuta alla natura del sistema che si sta progettando. Paradigma: Quale notazione e paradigma si utilizza per descrivere il modello, tra le più utilizzate si ricordano le State-Based Notations e le Transition-Based Notations. Criterio di selezione dei test: Il MBT può supportare diversi criteri di selezione a seconda delle esigenze del progetto in esame. Ad esempio l’approccio data-coverage può risultare particolarmente vantaggioso se si sta analizzando un sistema il cui funzionamento è 11 relativamente semplice ma avente una banca dati di grosse dimensioni. Tecnologia di generazione dei test: Il MBT ha la possibilità di automatizzare la generazione dei test direttamente dal modello. Le modalità di generazione sono diverse e anche in questo caso la scelta dell’approccio è circostanziale e dipendente dal dominio. Esecuzione dei test: Con l’offline testing abbiamo il semplice paradigma creazione del test, esecuzione del test. L’online testing consente una generazione dinamica dei test, infatti una volta che viene generato il test questo viene immediatamente eseguito ed analizzato. 1.4 Tools per il Model Based Testing. Nome Descrizione Ambiente sviluppo Licenza Spec Explorer 3.5 Prodotto da Microsoft. E’ un add-on di Visual Studio che genera test-suites da modelli scritti in C# , AsmL (Abstract state machine Language) o macchine a stati. Supporta sia l’offline che l’online testing e utilizza il criterio di copertura delle transizioni[7]. Visual Studio 2012 Microsoft (Commerciale / scopi di ricerca) FMBT Free model based Testing è un tool che genera casi di test scritti in nel linguaggio AAL/Python. Permette le generazione di test da macchine a stati finiti. La modellazione delle FSM avviene tramite GraphML (quindi non c’è supporto per le UML state machine). I modelli sono scritti come classi Java che interagiscono direttamente con il SUT. Utilizza le annotazioni native di Java 5.0. Utilizza le catene di Markov per generare modelli e test. Sfrutta il criterio di copertura “all-transitions”[8]. / LGPL (Open-source) MIT (Open-source) Graphwalker ModelJUnit MaTeLo Java Java GPL (Open-source) / Commerciale Si ricordano inoltre JUMBL e AETG che oggi non sono più in fase di sviluppo e il rilascio delle ultime versioni risale al 2010 e 2007 rispettivamente. JUMBL – J Usage Model Builder Library – Utilizza modelli statistici attraverso le catene di Markov. AETG – Automatic Efficient Test Generator è un servizio disponibile sul web che utilizza l’algoritmo pairwise per effettuare la copertura degli input. 12 Capitolo 2: Costruzione del modello. 2.1 Testing con macchina a stati finiti. Una macchina a stati finiti o FSM ( dall’inglese Finite State Machine) è un modello che permette di descrivere con precisione e in maniera formale il comportamento di molti sistemi. La rappresentazione grafica di un automa a stati finiti è il grafo. L’utilizzo di una FSM rientra tra le notazioni Transition-based , ovvero si descrive il sistema come una serie di stati e come questi siano tra loro collegati. In figura 5 è mostrato un esempio di FSM che modella un sistema adibito alla generazione di un numero casuale, si hanno: 2 stati : On e Off . Inizialmente lo stato è Off. 3 transizioni: Start/Stop/Generate. Per generare casi di test a partire dal modello, dobbiamo scegeliere un criterio di copertura, tra i quali si ricordano: All-states coverage: Ogni stato del modello è visitato Figura 5 - Esempio di macchina a stati finiti[9] almeno una volta. All-transitions coverage: Ogni transizione del modello è attraversata almeno una volta. All-transition-pairs coverage: Ogni paio di transizioni adiacenti deve essere attraversato almeno una volta. All-loop-free-paths coverage: Ogni percorso senza loop deve essere attraversato almeno una volta. Un loop si genera quando una transizione da uno stato ritorna allo stato stesso. Nell’esempio in figura 5 c’è un loop quando dallo stato off si effettua la transizione Generate. 13 All-paths-coverage: Ogni percorso deve essere attraversato almeno una volta. Most likely paths first: Priorità ai percorsi che hanno più probabilità di essere attraversati durante l’esecuzione. Shortest-paths first : Viene data priorità all’attraversamento dei percorsi più brevi. Si utilizza la seguente notazione {Stato attuale, Transizione, Stato prossimo}. Se si adottasse il criterio All-states sarebbe sufficiente adottare la seguente sequenza: 1. {Off, Start, On} C’è un solo step perché la macchina è inizialmente già nello stato Off (Initial state). Utilizzando All-transitions, invece: 1. {Off, Generate. Off} 2. {Off, Stop. Off} 3. {Off, Start. On} 4. {On, Start. On} 5. {On, Generate. On} 6. {On, Stop. Off} Quindi si nota che, con il modello in figura 5, si ha una sequenza molto più lunga utilizzando il criterio all-transitions piuttosto che quello all-states. Inoltre la copertura di tutte le transizioni implica la copertura di tutti gli stati. Conseguentemente un criterio di copertura è più forte dell’altro, come evidenziato dalla seguente gerarchia: Figura 6 - Gerarchia dei criteri di copertura transition-based. A →B significa che il criterio A è più forte del criterio B[10]. 14 2.2 Testing con Notazioni state-based (Pre/Post condizioni). Le notazioni State-based modellano il sistema come una collezione di variabili che rappresentano una “istantanea” dello stato interno del sistema e una serie di operazioni per modificare queste variabili. Ogni operazione è definita da pre-condizioni e una post-condizioni. Tra le notazioni più famose si ricordano B , VDM (Vienna Development Method) , JML (Java Modeling Language) , OCL (Object Constraint Language) e C#-plus-preconditions. Sono quattro i passi necessari per la scrittura di un modello pre/post condizioni. E’ necessario scegliere: Le caratteristiche del sistema che si vogliono testare. Le firme delle operazioni del modello. Le strutture dati utilizzate dal modello. Le pre-condizioni e post-condizioni per ogni operazione. Si sviluppa ora un esempio dove si ha un sistema il cui compito è quello di stabilire se, data la lunghezza dei tre lati, un triangolo è scaleno, isoscele o equilatero[11]. Le scelte delle caratteristiche da testare e delle strutture dati sono immediate. Infatti i requisiti del sistema sono chiari e il sistema è stateless, ovvero le uniche variabili sono gli input-output che classificano le operazioni. L’unica operazione è quella che riceve in input lato1,lato2,lato3 e restituisce in output il tipo di triangolo. Per quanto riguarda le pre-condizioni è necessario effettuare un controllo sul tipo in ingresso, in particolare vogliamo che i lati siano interi non negativi. 15 Utilizzando JML (Java Modeling Language) il metodo classifica è il seguente: //@ requires lato1 > 0 && lato2 > 0 && lato3 > 0; //@ requires lato1+lato2 > lato3 && lato2+lato3 > lato1 && lato1+lato3 > lato2; public static String classifica(int lato1 , int lato2 , int lato3) { if(lato1 == lato2 && lato2 == lato3) return "Equilatero"; else if(lato1==lato2 || lato1==lato3 || lato2==lato3) return "Isoscele"; else return "Scaleno"; } Con JML le pre-condizioni sono specificate attraverso la direttiva “requires”. Dove si specifica la non negatività dei lati e che la somma di due lati sia maggiore del lato rimanente. Utilizzando invece B-method si ha: 16 A questo punto è necessario utilizzare un criterio di copertura per effettuare la selezione dei casi di test. Si definisce condizione una espressione booleana che non contiene operatori booleani. Una decisione è una espressione booleana composta da più condizioni[12]. Decision Coverage (DC): Con questo metodo ogni decisione deve essere testata sia quando risulta true sia false. Nell’esempio della classificazione del triangolo in B, si hanno quattro decisioni: o Non negatività dei lati. o Somma di due lati maggiore del terzo. o Uguaglianza di tutti i lati. o Uguaglianza di una coppia di lati. Si hanno quindi 8 test in totale perché ogni condizione deve essere verificata per valori true e false (2x4). Tuttavia, poiché si hanno degli if innestati sono sufficienti cinque test per coprire tutte le decisioni. Condition/Decision coverage (C/DC): Richiede che siano testate sia le decisioni che le condizioni. Consegue che il numero di test da effettuare è maggiore, infatti è necessario testare ogni condizione sia per valori true che false. Modified Condition/Decision coverage (MC/DC): Questo criterio è un estensione di C/DC. Con il solo Condition Coverage non si riesce comprendere quale tra le tante condizioni va ad avere effetto sulla decisione. Con MC/DC si vuole dimostrare che ogni condizione all’interno di una decisione ha effetto indipendentemente dalle altre. Affinché ciò sia possibile si va a variare solo una particolare condizione mantenendo inalterate tutte le altre. In generale se si hanno N condizioni, vengono generati N+1 tests[13]. JMLUnitNG è un software che consente di generare automaticamente tests da Java Modeling Language. Andando ad eseguire JMLUnitNG su una classe contenente unicamente il metodo “classifica” descritto a pagina precedente, si ottiene un metodo per il framework TestNG che consente di generare una serie di tests. La generazione è semplice e avviene tramite il comando: “java –jar jmlunitng.jar [OPTIONS] path-list” [14]. Nel nostro caso il comando è: “java –jar jmlunitng.jar Classificatore.java”. 17 Si allega il log di tests generati automaticamente dal metodo: [TestNG] Running: Command line suite Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: Passed: constructor Classificatore() static classifica(-2147483648, -2147483648, -2147483648) static classifica(0, -2147483648, -2147483648) static classifica(2147483647, -2147483648, -2147483648) static classifica(-2147483648, 0, -2147483648) static classifica(0, 0, -2147483648) static classifica(2147483647, 0, -2147483648) static classifica(-2147483648, 2147483647, -2147483648) static classifica(0, 2147483647, -2147483648) static classifica(2147483647, 2147483647, -2147483648) static classifica(-2147483648, -2147483648, 0) static classifica(0, -2147483648, 0) static classifica(2147483647, -2147483648, 0) static classifica(-2147483648, 0, 0) static classifica(0, 0, 0) static classifica(2147483647, 0, 0) static classifica(-2147483648, 2147483647, 0) static classifica(0, 2147483647, 0) static classifica(2147483647, 2147483647, 0) static classifica(-2147483648, -2147483648, 2147483647) static classifica(0, -2147483648, 2147483647) static classifica(2147483647, -2147483648, 2147483647) static classifica(-2147483648, 0, 2147483647) static classifica(0, 0, 2147483647) static classifica(2147483647, 0, 2147483647) static classifica(-2147483648, 2147483647, 2147483647) static classifica(0, 2147483647, 2147483647) static classifica(2147483647, 2147483647, 2147483647) =============================================== Command line suite Total tests run: 28, Failures: 0, Skips: 0 =============================================== E’ importante notare che se non ci fossero le pre-condizioni, il metodo restituirebbe risultati errati. Infatti non ci sono controlli diretti per la non-negatività dei lati e la validità del triangolo. Tuttavia poiché scritte con annotazioni, anche queste ultime sono compilate implicando la validità del metodo. JMLUnitNG genera automaticamente un file dove è presente un metodo nel quale è possibile inserire dei semi che sono utilizzati per la generazione dei casi di test. Così facendo si ottiene un numero elevatissimo di test addizionali. Tutto ciò dimostra l’enorme potenziale del MBT: public RepeatedAccessIterator<?> packageValues() { return new ObjectArrayIterator<Object> (new Object[] { 5,12,4,21,99,5 /* Semi */ }); } 18 Si riporta l’output generato aggiungendo i semi 5,12,4,21,99,5: =============================================== Command line suite Total tests run: 513, Failures: 0, Skips: 0 =============================================== Aggiungendo 6 semi sono stati prodotti ben 513 tests. Il metodo classifica è testato per varie combinazione dei semi. Si allega un breve estratto: Passed: static classifica(0, 4, 0) Passed: static classifica(4, 4, 0) Passed: static classifica(5, 4, 0) Passed: static classifica(12, 4, 0) Passed: static classifica(21, 4, 0) Passed: static classifica(99, 4, 0) 2.3 Generazione dei test da modelli UML. Unified Modeling Language (UML) è ampiamente utilizzata per la creazione di modelli finalizzati all’utilizzo model-based testing[15]. Uno dei punti di forza di UML è l’ampia gamma di diagrammi che consentono di modellare il sistema sia con rappresentazioni statiche (e.g: class diagrams) sia dinamiche (e.g: activity diagrams). Inoltre è possibile completare ulteriormente i diagrammi attraverso OCL (Object Constraing Language). Non tutti i diagrammi UML sono utilizzati per effettuare MBT, i più utilizzati sono: Class diagrams: Rappresenta la struttura del sistema mostrando le sue classi con relativi attributi, operazioni e relazioni. Instance Diagrams: Diagramma utile per mostrare lo stato iniziale del sistema. State Machine diagrams: Una evoluzione dell’automa a stati finiti. Esprimono il comportamento di un oggetto attraverso transizioni e stati. Use case diagrams: Utilizzati per l’analisi dei requisiti. Inoltre con OCL è possibile aggiungere pre/post condizioni nei modelli per modellare il comportamento atteso del sistema. 19 Sia dato un sistema il cui scopo è la gestione di una pila di elementi generici[15]. Sulla pila sono definite le operazioni di push e pop. Gli elementi su cui effettuare la push sono scelti casualmente da un insieme. La pila ha una dimensione massima che è definita dalla costante MAX. Si riporta il diagramma delle classi in figura 7. La classe Stack possiede la costante MAX e la variabile size che riporta il numero corrente di elementi nella pila. Le operazioni sono quelle di push e pop. La classe Pool è l’insieme da cui vengono prelevati casualmente gli elementi per riempire lo Stack. Le operazioni di emptyOut e fillOut sono usate per svuotare o riempire la Pool. Utilizzando OCL sono così definite: Figura 7- Diagramma delle classi del sistema per la gestione di una pila[15]. context: Pool::emptyOut():OclVoid post: self.elements-> isEmpty() /*@REQ: pool_empty@*/ context: Pool::fillOut() : OclVoid post: self.elements = Element.allInstances() /*@REQ: pool_fill@*/ Ovvero dopo l’operazione di emptyOut c’è una post-condizione che la Pool sia vuota. Dopo l’operazione di fillOut la Pool deve essere piena. La classe Element rappresenta un generico elemento. 20 Si utilizza ora un diagramma State Machine per modellare dinamicamente il sistema: Figura 8- State machine diagram per il sistema gestione di una pila[15]. Lo stato di partenza è Empty, ovvero non ci sono elementi all’interno della pila. Se viene effettuata una pop viene lanciata un’eccezione EmptyStackException (Non si può effettuare una pop su una pila vuota) e si giunge nello stato di terminazione. Se invece si esegue una push, si giunge nello stato Loaded. Dallo stato Loaded si può tornare in Empty se il numero di elementi nella pila è zero. Invece giungo nello stato Full se il numero di elementi della pila raggiunge MAX-1. Oppure il sistema rimane nello stato Loaded se non sono verificate le precedenti due condizioni. Se si esegue una push mentre si è nello stato Full viene lanciata un’eccezione FullStackException e si giunge nello stato di terminazione. 21 Le transizioni pushOnEmptyStack , pushOnLoadedStack , popForEmptyStack,popForLoadedStack sono descritte attraverso OCL : action pushOnEmptyStack post: let element = self.pool.elements->any(true) in self.top = element /*@REQ:random_element@*/ and self.size = self.size + 1 and self.pool.elements = self.pool.elements->excluding(element) /*@REQ:automatic_delete@*/ action pushOnLoadedStack post: let element = self.pool.elements->any(true) in element.down = self.top /*@REQ:random_element@*/ and self.top = element and self.size = self.size + 1 and self.pool.elements = self.pool.elements->excluding(element) /*@REQ:automatic_delete@*/ action popForEmptyStack post: let element = self.top in self.top.oclIsUndefined() and self.size = self.size - 1 and self.pool.elements = self.pool.elements->including(element) /*@REQ:automatic_reinsertion@*/ action popForLoadedStack post: let element = self.top in self.top = element.down and element.down.oclIsUndefined() and self.size = self.size - 1 and self.pool.elements = self.pool.elements->including(element) /*@REQ:automatic_ reinsertion @*/ 22 A questo punto è possibile utilizzare uno strumento automatico per generare i Test targets e i veri e propri casi di test. I test targets descrivono quale elemento e quali requisiti vado a testare. Id Elemento UML testato. Definizione del target Contesto Requisiti testati Effetto Operazioni 1 POOL::emptyOut - elements->isEmpty() pool_empty 2 POOL::fillOut - elements=Element.allInstances() pool_fill 3 Empty EmptyStackException - true empty_stack_exception 4 Empty Loaded - let element = pool.elements->any(true) in top=element and size=size+1 and pool.elements->excludes(element) random_element, automatic_delete 5 Loaded Loaded size < max-1 let element = pool.elements->any(true) in element.down=top and top=element and size=size+1 and pool.elements->excludes(element) random_element, automatic_delete 6 Loaded Full size = max1 let element = pool.elements->any(true) in element.down=top and top=element and size=size+1 and pool.elements->excludes(element) random_element, automatic_delete 7 Full FullStackException - true full_stack_exception 8 Full Loaded - let element = top in top=element.down and element.down.oclIsUndefined() and size=size-1 and pool.elements->includes(element) automatic_reinsertion 9 Loaded Loaded size > 1 let element = top in top=element.down and element.down.oclIsUndefined() and size=size-1 and pool.elements->includes(element) automatic_reinsertion 10 Loaded Empty size = 1 let element = top in top.oclIsUndefined() and size=size-1 and pool.elements->includes(element) automatic_reinsertion Transizioni 23 Si riportano nella seguente tabella i test generati. Questi sono composti da: Preamble: Sequenza di operazioni necessarie per giungere al punto che si sta testando Body: L’esecuzione del target. Postamble: Sequenza di operazioni per tornare allo stato iniziale del modello. Target Id Test corrispondente Preamble Body postamble 1 pool.emptyOut() 2 pool.fillOut() 3 stack.pop() pool.emptyOut() 4 pool.fillOut() stack.push() stack.pop(), pool.emptyOut() 5 pool.fillOut(), stack.push() stack.push() stack.pop(), stack.pop(), pool.emptyOut() 6 pool.fillOut(), stack.push(), stack.push() stack.push() stack.pop(), tack.pop(), stack.pop(), pool.emptyOut() 7 pool.fillOut(), stack.push(), stack.push(), stack.push() stack.push() 8 pool.fillOut(), stack.push(), stack.push(), stack.push() stack.pop() stack.pop(), stack.pop(), pool.empty() 9 pool.fillOut(), stack.push(), stack.push() stack.pop() stack.pop(), pool.empty() 10 pool.fillOut(), stack.push() stack.pop() pool.empty() Si prenda ad esempio il test numero 3. Si vuole testare la transizione che dallo stato Empty lancia un’eccezione causata da una pop. Preamble è vuoto, in quanto lo stato Empty è proprio lo stato iniziale. Per testare la transizione viene eseguito il metodo stack.pop() . Postamble è vuoto in quanto il sistema, dopo l’operazione, va nello stato di terminazione e non è possibile ritornare allo stato iniziale. 24 Capitolo 3: Caso di studio: classe File. Sia data la seguente interfaccia File dove sono definite le seguenti operazioni: public interface File { public int read(); public void write(); public void open(); public void close(); } Si supponga che un utilizzatore di tale interfaccia vada a richiamare l’operazione di read() prima di effettuare una open() . Ciò che succede è che viene lanciata un’eccezione, dovuta al fatto che non è possibile leggere un file che non sia stato precedentemente aperto. Ovvero l’interfaccia non ha alcuna informazione circa l’ordine con cui devono essere effettuate le operazioni. In questa ottica il model-based testing può essere di aiuto, specialmente grazie a diagrammi che specificano lo stato del sistema in ogni momento. 3.1 Creazione del modello. Si vuole utilizzare l’approccio Model-based per generare casi di test per una classe File su cui sono definite le operazioni di : Open: Apertura del file. Close: Chiusura del file. Read: Lettura da file. Write: Scrittura su file. 25 Si utilizza una macchina a stati del protocollo per modellare il sistema: Figura 9 - Diagramma a stati finiti del sistema File. Si ricava la tabella degli stati: STATO INIZIALE OPERAZIONE STATO FINALE Chiuso Open Aperto Aperto Write Aperto Aperto Read Aperto Aperto Close Chiuso A questo punto si sceglie un criterio di copertura. Utilizzando all-transitions si ottiene la seguente sequenza: 1 Open. 2 Read. 3 Write. 4 Close. 26 3.2 Implementazione del modello della FSM in Java. E’ possibile utilizzare una modifica dello State design pattern per ottenere una implementazione della FSM[16]. In particolare si utilizzano le classi: 1 FSM : Incapsula l’array degli stati e lo stato corrente. 2 State: Classe astratta da cui ereditano gli stati Aperto e Chiuso. 3 Aperto: Rappresenta lo stato Aperto. 4 Chiuso: Rappresenta lo stato Chiuso. 5 StateDemo: Contiene il metodo main che genera gli input. Si riportano le classi Aperto e Chiuso dove sono definite le operazioni: class Aperto extends State { public void open(FSM fsm) { System.out.println("Aperto + open fsm.changeState(STATO_APERTO); } = Aperto"); public void close(FSM) { System.out.println("Aperto + close = CHIUSO"); fsm.changeState(STATO_CHIUSO); } public void read(FSM fsm) { System.out.println("Aperto + read = Aperto"); fsm.changeState(STATO_APERTO); } public void write(FSM fsm) { System.out.println("Aperto + write = Aperto"); fsm.changeState(STATO_APERTO); } } class Chiuso extends State { public void open(FSM fsm) { System.out.println("Chiuso + open fsm.changeState(STATO_APERTO); } = Aperto"); public void close(FSM fsm) { System.out.println("Chiuso + close = Chiuso"); fsm.changeState(STATO_CHIUSO); } public void read(FSM fsm) { System.out.println("ERRORE: READ DI UN FILE CHIUSO"); } public void write(FSM fsm) { System.out.println("ERRORE: WRITE DI UN FILE CHIUSO"); } } 27 Si noti come sono presenti due operazioni che restituiscono errori, queste sono read/write di un file chiuso. Infatti non è possibile trovare le corrispondenti transizioni sul modello a stati. Per testare il nostro modello si utilizza un generatore random di interi. Ogni intero corrisponde ad una particolare operazione: static static static static final final final final int int int int READ = 0; WRITE = 1; OPEN = 2; CLOSE = 3; Si crea un ciclo for per inserire tali interi nell’array delle operazioni in input. for(int i = 0; i < NUMERO_OPERAZIONI; i++) { input.add(r.nextInt(operazioni.length)); } Dove NUMERO_OPERAZIONI è il numero di operazioni da generare, maggiore è il numero più a fondo sarà testato il nostro sistema. A questo punto a seconda dell’intero presente nell’array si effettua la corrispondente operazione: for (int i = 0; i < input.size(); i++) if (input.get(i) == READ) fsm.read(); else if (input.get(i) == WRITE) fsm.write(); else if (input.get(i) == OPEN) fsm.open(); else if(input.get(i) == CLOSE) fsm.close(); } 28 I risultati dell’esecuzione del software sono analizzati di seguito: Input Output OPEN Chiuso + open = Aperto READ Aperto + read = Aperto OPEN Aperto + open = Aperto WRITE Aperto + write = Aperto WRITE Aperto + write = Aperto WRITE Aperto + write = Aperto WRITE Aperto + write = Aperto WRITE Aperto + write = Aperto CLOSE Aperto + close = Chiuso READ ERRORE: READ DI UN FILE CHIUSO Seconda esecuzione: Output Input READ ERRORE: READ DI UN FILE CHIUSO READ ERRORE: READ DI UN FILE CHIUSO READ ERRORE: READ DI UN FILE CHIUSO WRITE ERRORE: WRITE DI UN FILE CHIUSO WRITE ERRORE: WRITE DI UN FILE CHIUSO CLOSE Chiuso + close = Chiuso OPEN Chiuso + open = Aperto READ Aperto + read = Aperto READ Aperto + read = Aperto WRITE Aperto + write = Aperto 29 3.3 Implementazione delle operazioni con RandomAccessFile. Si utilizza ora la classe RandomAccessFile per implementare le operazioni di Read, write, open e close. Si inserisce il file nella cartella del progetto e ne dichiariamo la variabile nella classe FSM. public RandomAccessFile file; A questo punto si modificano i metodi delle classi Aperto e Chiuso come segue: open fsm.file = new RandomAccessFile("file.txt", "rw"); close fsm.file.close(); read int aByte = fsm.file.read(); write fsm.file.write("Helloworld,".getBytes()); Eseguendo il programma si nota che le operazioni che precedentemente restituivano un errore in output, ora lanciano un’eccezione. Ad esempio, generando la seguente sequenza: 0) 1) 2) 3) 4) 5) 6) 7) 8) 9) OPEN READ CLOSE READ CLOSE CLOSE WRITE WRITE OPEN READ Viene lanciata un’eccezione IOException al passo 3,4,5,6,7 in quanto si esegue una READ/WRITE su un file chiuso. 30 java.io.IOException: Stream Closed at java.io.RandomAccessFile.read0(Native Method) at java.io.RandomAccessFile.read(RandomAccessFile.java:320) at Chiuso.read(StateDemo.java:128) at FSM.read(StateDemo.java:27) at StateDemo.main(StateDemo.java:171) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134) Generando la seguente sequenza: 0) 1) 2) 3) 4) 5) 6) 7) 8) 9) OPEN WRITE WRITE WRITE CLOSE READ READ OPEN CLOSE WRITE Vengono generate eccezioni al passo 5-6 (Read di un file chiuso) e al passo 9 (Write di un file chiuso). Si riporta l’eccezione nella Read: java.io.IOException: Stream Closed at java.io.RandomAccessFile.read0(Native Method) at java.io.RandomAccessFile.read(RandomAccessFile.java:320) at Chiuso.read(StateDemo.java:128) at FSM.read(StateDemo.java:27) at StateDemo.main(StateDemo.java:171) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134) 31 Conclusioni La creazione di un modello del sistema finalizzato alla generazione di casi di test richiede un grosso investimento economico. Il Model based testing è vantaggioso a lungo termine, specialmente per quanto riguarda la facile manutenibilità dei casi di test. Tuttavia è prevista una lunga fase di apprendimento dei tools e delle tecniche per la modellazione che rendono ostico avvicinarsi a tale approccio. Negli ultimi anni è aumentato l’interesse per il MBT: ci sono state diverse pubblicazioni dove si evidenzia chiaramente il fatto che la generazione automatica di test da modello sarà largamente utilizzata in futuro [6]. Ci sono però ancora due problematiche da affrontare: se il MBT sia cost-effective e se potrà essere utilizzato per effettuare testing di requisiti non funzionali. Mentre ci sono studi che evidenziano che il MBT sia vantaggioso da un punto di vista dei costi, è ancora molta la strada per giungere alla creazione di test per specifiche non funzionali da un modello del sistema. 32 Bibliografia [1] Scott W. Ambler, Examining the Agile Cost of Change Curve, http://www.agilemodeling.com/essays/costOfChange.htm [2] Veanes,Campbell,Schulte,Tillman , Online Testing with Model Programs, Microsoft Research – Redmond.. [3] Microsoft Spec Explorer MBT, Model based testing advantages, https://msdn.microsoft.com/en-us/library/ee620469.aspx. [4] Craggs, Sardis , Heuillard, AGEDIS Case Studies: Model-based Testing in Industry https://www.research.ibm.com/haifa/projects/verification/mdt/papers/AGEDIS_in_Industry.pdf [5] Utting , Legeard – Practical Model Based Testing . Par. 2.8 [6] Utting , Pretschner, Legeard – A taxonomy of Model based testing . April 2006. [7] Olli-Pekka Puolitaival - VTT technical Research centre of Finland – Model based Testing tools. [8] Zoltàn Micskei , Model Based testing - MBT tools http://mit.bme.hu/~micskeiz/pages/modelbased_testing.html [9] CodeProject – Generic Finite State Machine (FSM) – 2 Jul 2012 http://www.codeproject.com/Articles/406116/Generic-Finite-State-Machine-FSM [10] Utting – Legeard – Practical Model Based Testing – Capitolo 4 – Selecting your tests. [11] Glenford Myers – The art of Software testing - Triangle classification program. [12] Certification Authorities Software Team – What is a “Decision” in Application of MC/DC and DC. – June 2002 [13] Christophe Sourisse, Verifysoft Technology - Example of Modified condition/decision coverage (MC/DC - MCDC). 33 [14] Applied Formal Methods Group - Institute of Technology University of Washington Tacoma - JMLUnitNG Usage - http://formalmethods.insttech.washington.edu/software/jmlunitng/usage.html [15] F.Bouquet , C.Grandpierre, B. Legeard, M.Utting – A subset of precise UML for Modelbased testing. [16] Sourcemaking - State in Java: Distributed transition logic http://sourcemaking.com/design_patterns/state/java/6 34