Metodi e strumenti per il model-based testing.

annuncio pubblicitario
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
Scarica