Strumenti a supporto del testing automatico di software sviluppato in

Transcript

Strumenti a supporto del testing automatico di software sviluppato in
Scuola Politecnica e delle Scienze di Base
Corso di Laurea in Ingegneria Informatica
Elaborato finale in Ingegneria del Software e dei Dati
Strumenti a supporto del testing
automatico di software sviluppato in
linguaggio C/C++
Anno Accademico 2013/2014
Candidato:
Alessandro Recano
matr. N46000436
Indice
INTRODUZIONE
1
1 Il Testing
3
1.1 Una definizione di Testing . . . . . . . . . . . . . . . . . . . . . . . .
3
1.2 I principi del Testing . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.3 Livelli di Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.4 Tecniche di Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2 Il Testing Automatico
8
2.1 Il processo di automazione del collaudo . . . . . . . . . . . . . . .
8
2.1.1 Generazione dei Casi di Test . . . . . . . . . . . . . . . . . .
8
2.1.2 Preparazione ed Esecuzione del Test . . . . . . . . . . . . .
9
2.1.3 Valutazione dell’efficacia di Test Suite . . . . . . . . . . . .
10
2.2 Test d’Unità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
2.2.1 Strumenti a supporto di test d’unità . . . . . . . . . . . . .
11
2.2.2 Esempio di Unit Testing con CppUnit . . . . . . . . . . . .
13
2.3 Control Flow Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
2.3.1 Strumenti a supporto della generazione automatica di Control Flow Graph . . . . . . . . . . . . . . . . . . . . . . . . . .
17
2.3.2 Understand - Source Code Analysis & Metrics . . . . . . .
17
2.3.3 Analisi di OperazioniBase attraverso Understand . . . . .
18
2.4 Analisi di Copertura . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
2.4.1 Strumenti a supporto di Code Coverage automatico . . .
20
2.4.2 Analisi di Copertura con gcov . . . . . . . . . . . . . . . . .
21
Conclusioni
23
INTRODUZIONE
Nella società moderna, la quasi totalità dei servizi è svolta e offerta attraverso il
supporto di sistemi informatici. Un requisito essenziale e non poco complesso,
è diventato quindi il concetto di affidabilità, un parametro esterno della qualità
del software che ne misura la bontà in termini di soddisfazione dell’utente.
Mantenere un grado di affidabilità elevato, non significa solo scrivere buon
codice, bensì pianificare ed eseguire una serie di attività che consentano allo
sviluppatore di verificare l’adeguatezza di ogni singolo stadio del processo di
sviluppo del software.
Tale strategia richiede spesso costi di sviluppo ingenti, causa per cui l’industria del software moderna ha cominciato a considerare economicamente vantaggiosa, per applicazioni non critiche, l’alternativa di rilasciare prodotti con un
tasso iniziale di malfunzionamenti piuttosto elevato, in modo tale da correggere
i bug attraverso aggiornamenti su segnalazione.
Quando si parla di testing si pensa ad un’attività noiosa e impegnativa in termini
di tempo, tralasciando che talvolta, oltre alla produzione di buona documentazione, pianificare ed eseguire un processo di collaudo possa portare numerosi
vantaggi utili in caso di futuri cambiamenti.
Grazie ad un buon testing si possono:
* Ridurre i cicli di sviluppo evitando la manifestazione di errori in stadi
successive del processo di sviluppo.
* Semplificare operazioni di refactoring
* Aumentare la produttività ottenendo prodotti di qualità risparmiando
in termini di risorse.
Si può quindi affermare che l’attività di test ha un ruolo essenziale nel processo di sviluppo del software, nonostante sia complessa e laboriosa.
1
INDICE
Scopo di questo elaborato, sarà la descrizione in maniera dettagliata del processo di collaudo automatico del software, esplorando le soluzioni a supporto di
software sviluppato in linguaggio C/C++.
2
Capitolo 1
Il Testing
Il test di un programma può rilevare la presenza di malfunzionamenti, ma
non dimostrarne l’assenza. (E. Dijkstra)
1.1
Una definizione di Testing
L’attività di testing del software, chiamata talvolta collaudo, gira attorno a due
semplici parole: failure (malfunzionamento) e fault (difetto). Occorre trattare questi due concetti con meticolosa attenzione, in quanto seppur sembrino
simili, hanno un significato diverso e ben preciso. Quando si parla di malfunzionamenti ci si riferisce alla situazione in cui il sistema non fa ciò che l’utente
si aspetta; mentre i difetti identificano una particolare sequenza di istruzioni
che, con determinati dati in ingresso, genera malfunzionamenti.
In accordo con lo standard IEEE,
Software testing is the process of analyzing a software item to detect the
differences between existing and required conditions (that is, bugs) and
to evaluate the features of the software item.
Tale processo rappresenta la parte di analisi dinamica di un processo di revisione più ampio chiamato Verifica & Validazione, che si prepone l’obiettivo di
verificare che il software prodotto rispecchi i requisiti richiesti e lo faccia nella
maniera dovuta.
Oggigiorno l’attività di Collaudo del Software non è solo parte del ciclo di
vita del software, bensì un vero e proprio modello di sviluppo, che con il tempo
ha ha dato vita alla tecnica di Test-Driven Developement (TDD)
3
1. Il Testing
1.2
I principi del Testing
Come già descritto, l’obiettivo principale della fase di testing è quello di scovare
malfunzionamenti al fine di permettere la rimozione dei bug durante la fase di
debugging.
Quando si segue il ciclo di sviluppo del software, una delle prime scelte da
fare è sicuramente quella relativa alla pianificazione dell’attività di testing.
Si possono seguire due diversi approcci:
A cascata : prevede che l’esecuzione del test sia avviata solo a sviluppo concluso. É sicuramente una tecnica più semplice, ma può comportare maggiore
impiego di risorse economiche dato che eventuali fallimenti sono scovati
solo alla fine della progettazione.
Iterativo incrementale : è affiancato alla fase di progettazione e produzione
del software, e permette di avere un processo di sviluppo più controllato,
sebbene richeda uno sforzo maggiore nella pianificazione delle attività.
Quando un test non trova malfunzionamenti, esso si dice ideale. La condizione di idealità, però, sebbene può lasciar intendere che il software è pronto
per il rilascio perché privo di difetti, può nascondere una scarsa qualità dell’attività di testing. Inoltre, dato che la correttezza di un programma risulta essere
un problema indecidibile, non vi è garanzia, che un modulo o un sistema dopo
n test nei quali non sono stati riscontrati malfunzionamenti, risponda alla stessa
maniera.
Per classificare la qualità di un processo di testing, si fa uso di due parametri
fondamentali:
Efficienza : indica il numero di malfunzionamenti evidenziati in rapporto ai
casi di test effettuati. É un parametro che permette di mantenere bassi i
costi dell’attività di testing, sebbene un abuso possa comportare difficoltà
nel rilevare la provenienza del malfunzionamento.
Efficacia : è un valore statistico che indica la scoperta di quanti più malfunzionamenti possibili in rapporto a quelli totali. Se privilegiato, è un valore
che consente di mantenere un elevato grado di affidabilità.
4
1. Il Testing
1.3
Livelli di Testing
Una prima classificazione del Testing può essere fatta secondo livelli. Attraverso
questo criterio, si può parlare di:
* Livello Produttore (Testing di Unità, Sistema, Integrazione)
* Livello Cooperativo-Produttore/Utente Privilegiato (Alpha Testing, Beta Testing)
* Livello Utente (Testing di Accettazione)
Figura 1.1: Modello a V del Testing del Software
Per quanto concerne il Livello Produttore, si inizia effettuando il test della
più piccola unità di codice (funzioni, oggetti e componenti riusabili) per poi
passare al test di unità integrate al fine di formare un sistema o sotto-sistema.
Si termina quindi con l’esecuzione di un test di Integrazione, che richiede la
costruzione del sistema a partire dai suoi componenti e prevede un collaudo
mirato all’identificazione di eventuali problemi di interazione tra le unità. Può
essere realizzato attraverso approcci top-down, bottom-up, o misto.
Il Livello Cooperativo-Produttore/Utente Privilegiato, prevede invece due
fasi di Testing tipicamente adoperate dai produttori di packages per mercati
5
1. Il Testing
di massa. La fase Alpha prevede l’utilizzo del sistema da parte di utenti reali
nell’ambiente di produzione.
La fase Beta, invece, prevede che il software sia utilizzato in ambiente reale.
Entrambe le fasi sono antecedenti all’immissione del prodotto finale sul mercato.
Il Testing di Accettazione, appartente al Livello Utente, può essere inquadrato
più come una dimostrazione che un vero e proprio test. L’obiettivo è quello di
mettere il cliente in condizione di decidere se accettare o meno il prodotto. É
tipicamente un test basato solo sulle specifiche del software e viene effettuato
sull’intero sistema, dal momento in cui bisogna provare che il prodotto offra le
funzionalità, le prestazioni e l’affidabilità richieste dal committente.
1.4
Tecniche di Testing
Per quanto riguarda le tecniche di Testing, si può principalmente fare una distinzione tra Testing funzionale (o Black Box) e Testing strutturale (o White
Box).
La tecnica Black Box, detta anche a scatola nera, richiede l’analisi degli output generati dal sistema (o dai suoi componenti) in risposta ad input formulati
sulla sola conoscenza dei requisiti, nascondendo in tal modo i dettagli implementativi al tester. Per pianificare un tale processo di test si fa affidamento sulle
specifiche implementative dalle quali si ricavano le classi di equivalenza.
L’approccio White Box, contrariamente alla tecnica Black Box permette un’analisi più dettagliata dal momento in cui analizza i cambiamenti del software
in base agli input foniti dall’utente. É una tecnica ”strutturale”, e prevede che
il tester sia a conoscenza della struttura interna del programma da collaudare. Il numero di casi di test, seguendo tale approccio, sarà maggiore, dovendo
rispettare diversi criteri come:
Statement coverage : Consente di stabilire se ogni istruzione del codice sia
eseguita almeno una volta attraverso la scelta di un insieme di input.
Branch coverage : Bisogna esaminare le ramificazioni del diagramma di flusso
in corrispondenza di salti condizionati. Bisogna accertare che ogni decisione non degeneri, ma che sia sempre possibile giungere in un nodo nel
grafo.
6
1. Il Testing
Condition coverage : Bisogna progettare un insieme di casi di test che permettano di far assumere a ciascuna condizione elementare tutti i valori
possibili.
Branch and Condition coverage : Bisogna ricavare un insieme di casi di test
per cui ogni condizione assume un valore di verità e falsità almeno una
volta e che ogni decisione sia percorsa almeno una volta sia per il ramo
vero sia per il ramo falso.
Multiple conditions coverage : Prevede la generazione di un insieme di casi di test che consentano di verificare tutti i possibili valori di ciascuna
combinazione di condizioni presente in una decisione.
Path coverage : Rappresenta l’analisi dei cammini percorribili nel codice sorgente e la ricerca dei cammini che abbiano un costo minore. Tale tecnica
è affiancata all’utilizzo del Control Flow Graph (CFG).
7
Capitolo 2
Il Testing Automatico
2.1
Il processo di automazione del collaudo
Il processo di automazione del collaudo del software nasce con l’obiettivo di
ridurre lo sforzo umano e la quantità di errori prodotti durante la fase di Testing. Sebbene un collaudo manuale preveda il test del software attraverso continue sessioni prova, spesso può risultare oneroso in termini di tempo e costi.
Un’attività di testing automatizzato, se pianificata e progettata a dovere, può
favorire, invece, fattori come il riuso del progetto, affidabilità e minimizzazione
degli errori nella stesura dello stesso. In generale, automatizzare un collaudo
vuol dire scrivere codice esterno al programma da collaudare, ma che interagisca con quest’ultimo al fine di rilevare i malfunzionamenti in maniera del tutto
automatica.
Nel processo di automazione di un collaudo, si individuano tre principali
aree di intervento:
• Generazione dei Casi di Test
• Preparazione ed Esecuzione del Test
• Valutazione dell’efficacia di Test Suite
2.1.1
Generazione dei Casi di Test
La fase di preparazione dei Casi di Test, è uno di quei task che può essere automatizzato in modo tale da alleggerire l’attività di Test Design. Per ottenere un
Test efficace, bisogna però valutare anche l’efficacia dei casi test ed eliminare
eventuali ridondanze.
8
2. Il Testing Automatico
Un esempio di scenario propenso all’automazione potrebbe essere un sistema di logging che registra (fase di Capture) e riproduce (fase di Replay) le
interazioni tra l’utente ed il sistema durante una User Session. Tale situazione,
comporta però lo studio di numerosi casi di test, dal momento in cui bisognerebbe ipotizzare la concorrenza di un gran numero di utenti nel sistema. Per
ovviare al problema della generazione di tali test case, possono essere sfruttate
tecniche di generazione automatica basate su test case già esistenti.
Il Testing Mutazionale è una di queste tecniche. Prevede l’applicazione di operatori di mutazione che modificano/incrociano i dati dei test case esistenti, in
modo tale da ottenerne dei nuovi. Generalmente, i nuovi test case variano; se
le sequenze di input sono numeriche, nel segno, in valore (es. viene raddoppiato), etc..
Con tale tecnica che viene principalmente applicata per testing di interfacce o
protocolli, si possono ottenere con sforzo minore Test Suite più piccole e con
maggiore copertura.
2.1.2 Preparazione ed Esecuzione del Test
Nell’ambito della eXtreme Programming si collocano framework di supporto all’esecuzione dei casi di test. Tali framework, nascono come soluzione per agevolare in particolar modo i Test d’Unità; sebbene possano essere adattati in
generale alle problematiche di testing Black Box.
I più conosciuti appartegono alla famiglia xUnit:
• JUnit (Java)
• CppUnit (C++)
• csUnit (C#)
• NUnit (.NET framework)
• HttpUnit e HtmlUnit (Web Application)
Come già detto, tali framework nascono principalmente come supporto allo
Unit Testing, tecnica che prevede il collaudo da parte dello sviluppatore stesso.
Eseguire un Test d’Unità, significa molto spesso, riscrivere la classe/componente all’interno del modulo principale del programma (main) e ciò comporta una
serie di problemi dal momento in cui il prodotto finale verrebbe distribuito con
9
2. Il Testing Automatico
tale classe di Test e di conseguenza appesantito.
Scopo di tali framework è quindi utilizzare un approccio sistematico che consenta di separare il codice del test da quello dell’unità da collaudare, che supporti
la strutturazione dei casi di test in Test Suite e che fornisca un output separato
dall’output della classe originale.
2.1.3
Valutazione dell’efficacia di Test Suite
Una misura di efficacia è data dal grado di Copertura raggiunto dalla Test Suite.
Dato un criterio di copertura, però, è abbastanza semplice verificare quanto
questo sia efficace misurando la copertura della Test Suite con tale criterio; ma è
abbastanza complesso generare casi di test che garantiscano massima copertura.
Un possibile approccio sarebbe lo sviluppo di appositi programmi che inseriscano all’interno del software delle sonde, con il compito di valutare l’efficacia
della Test Suite rispetto ai criteri di copertura strutturali.
Più in generale, quando si parla di valutazione dei risultati di un test, ci si riferisce all’utilizzo dell’Oracolo, che ha il compito di confrontare i risultati del test
con quelli attesi (l’Oracolo conosce a priori il comportamento del componente
sottoposto a collaudo).
2.2
Test d’Unità
Il Test d’Unità, chiamato anche Test dei Componenti, è una tecnica di collaudo
che prevede l’analisi di un frammento di software, che sia una routine, un modulo o una classe. Tale tecnica prevede che sia lo sviluppatore stesso ad eseguire
il test, ed è suddivisa in tre momenti:
Settaggio Precondizioni É la fase in cui il tester imposta le precondizioni che
il test assume verificate prima della sua esecuzione.
Test Rappresenta la fase di esecuzione vera e propria del codice di testing.
Ripristino condizioni Fase in cui le condizioni vengono ripristinate allo stato
antecedente il test.
Altri parametri importanti da tenere in considerazione per l’esecuzione di
un buon test d’unità sono la compattezza ed il tempo d’esecuzione, che non
deve superare l’ordine dei millisecondi. Se ciò accade, è opportuno suddividere
10
2. Il Testing Automatico
il test in sezioni più piccole, in modo tale da garantire l’indipendenza dei test
cases.
2.2.1
Strumenti a supporto di test d’unità
Gli strumenti che offrono supporto automatico per il Test d’Unità sono davvero
tanti. Di seguito sono elencati i principali framework (con licenza Open Source
e Commerciale) disponibili sul mercato.
Nome
xUnit
Fixtures
Mocks
Eccezioni Macro
Licenza
Cantata
No
Si
Si
Si
Si
Commerciale
CUTE
Si
Si
Si
Si
No
Open Source
Parasoft
Si
Si
Si
Si
Si
Commerciale
Si
Si
No
Si
Si
Open Source
C/C++
Test
CppUnit
Cantata : É un software con licenza commerciale rilasciato da QA-Systems. É
una soluzione che comprende una suite di strumenti per test d’unità e
d’integrazione su host computer o piattaforme target embedded. É fortemente automatizzato al fine di garantire la riusabilità dei componenti di
test e la generazione di script per software sviluppato in C/C++. Possiede
inoltre un’interfaccia unica di controllo per simulare ed intercettare chiamate a routine di collaudo ed uno strumento integrato di code coverage
per l’analisi nei linguaggi C/C++ e Java.
CUTE : CUTE è l’acronimo di C++ Unit Testing Easy Environment ed è un programma portabile per testing d’unità di applicazioni scritte in C++. L’ultma release è la 4.4.0 ed è disponibile inoltre come Plugin per Eclipse. La
caratteristica principale è la semplicità, difatti CUTE mette a disposizione
una forte automazione in termini di progettazione di Test Suite e preparazione all’esecuzione del test. Tale automazione è favorita grazie ad un
file chiamato autocutee.mk, che incluso nel Makefile si occupa di tutte le
attività di gestione. Nell’header cutee.h sono definite una serie di macro
che consentono di creare una classe di test in maniera molto semplice.
Basta infatti usare la macro TEST_CLASS per creare la classe in questione, e TEST_FUNCTION per aggiungervi un metodo di prova. Va infine
11
2. Il Testing Automatico
modificato il file test_files aggiungendo il nome del test case. Ogni classe
di test produce un separato file oggetto di modo tale che, alla modifica del
programma da collaudare, vengano compilate solo le classi che risentono
di modifiche.
Parasoft C/C++ Test : É uno strumento commerciale che integra analisi e revisione del codice, test d’unità, code coverage ed infine test di regressione.
Funziona sia in ambiente IDE che in batch mode. Supporta la generazione automatica dei test cases al fine di verificare input inaspettati, e più in
generale per verificare il comportamento del sorgente. Il punto di forza
di tale framework è la capacità di identificare errori a run-time senza eseguire il software, con supporto ai problemi più comuni della programmazione quali uso di memoria non allocata, dereferenziazione di puntatori,
overlfows di array e buffer e memory leaks.
CppUnit : CppUnit è uno dei più conosciuti framework per il testing d’unità
di applicazioni scritte in linguaggio C++, sviluppato da MICHAEL FEATHERS .
Esso rappresenta un’evoluzione del famosissimo JUnit, nato nel
1997 grazie a ERICH GAMMA e KENT BECK.
L’ultima ed attuale versione del software è la 1.13.2, distribuita in duplice
libreria (una per la configurazione Debug, l’altra per la configurazione Release) con licenza LGPL (GNU Lesser General Public License). Tale versione è inoltre utilizzata in distribuzioni Linux come Debian, Ubuntu, Gentoo
ed Arch. Il cardine di tale framework è la semplicità con cui è possibile
realizzare test case e test suite. Basta infatti scrivere poche righe di codice
per costruire un classe di test.
Altre features che arricchiscono questo framework sono la possibilità di
esportare il log del test in un file XML, possibilità di controllare i risultati
del test come evento di post-compilazione nella console dell’IDE scelto,
la possibilità di utilizzare macro per la semplificazione della dichiarazione delle classi di test (chiamate HELPER_MACROS) e un registro (chiamato test registry nella libreria), che consenta di tener traccia di tutte le
chiamate alle routine di collaudo effettuate di modo tale da non dover
ricompilare il progetto.
12
2. Il Testing Automatico
2.2.2
Esempio di Unit Testing con CppUnit
Completata la configurazione delle librerie attraverso l’aggiunta di direttive di
linker e di post-compilazione nell’IDE (si è scelto per questa dimostrazione Eclipse e la versione 1.12.1 del framework), l’utilizzo di CppUnit è davvero elementare.
Per semplicità si è scelto di testare la classe OperazioniBase che contiene le
implementazioni delle 4 operazioni matematiche elementari.
1
c l a s s OperazioniBase
2
{
3
public :
4
O p e r a z i o n i B a s e (void ) { }
5
~O p e r a z i o n i B a s e (void ) { }
6
int Somma( int x , int y ) { return x+y ; }
int D i f f e r e n z a (int x , int y ) { return x−y ; }
int P r o d o t t o (int x , int y ) { return x ∗ y ; }
double Rapporto (double x , double y ) { return x/y ; }
7
8
9
10
11
};
La definizione e l’implementazione delle routine di test è quindi possibile
attraverso la dichiarazione di una nuova classe, chiamata in tal caso TesterClass,
la cui interfaccia contiene i metodi e le chiamate necessarie all’esecuzione dei
test. Tale classe è ereditata dalla classe TestFixture di CppUnit, che consente
di avere un ambiente comune per un set di test case, ed implementa i metodi
di SetUp e TearDown, che come descritto nel paragrafo precedente, hanno il
compito di settare precondizioni e ripulire le stesse al termine dei test.
1
c l a s s T e s t e r C l a s s : p u b l i c CppUnit : : T e s t F i x t u r e
2
{
3
public :
4
T e s t e r C l a s s (void ) ;
5
~T e s t e r C l a s s (void ) ;
6
7
void test_somma ( )
8
{
9
O p e r a z i o n i B a s e op ;
10
int a = 10 , b = 15;
11
12
CPPUNIT_ASSERT_EQUAL ( a+b , op . Somma( a , b ) ) ;
}
13
14
void t e s t _ d i f f e r e n z a ( ) { . . . }
13
2. Il Testing Automatico
15
void t e s t _ p r o d o t t o ( ) { . . . }
16
17
void t e s t _ r a p p o r t o ( ) { . . . }
18
19
};
La routine test_somma mostra come sia possibile attraverso un’istanza della
classe OperazioniBase e la chiamata alla macro
CPPUNIT_ASSERT_EQUAL(expected, actual) testare il metodo Somma attraverso gli assert offerti dal framework, riepilogati nella tabella seguente.
Figura 2.1: Assert del Framework CppUnit
A questo punto, appena prima la dichiarazione dei membri publici di TesterClass, basterà effettuare chiamate a macro che creano la test suite, vi aggiungono i test case desiderati e ne terminano l’esecuzione.
1
c l a s s T e s t e r C l a s s : p u b l i c CppUnit : : T e s t F i x t u r e
2
{
3
CPPUNIT_TEST_SUITE ( T e s t e r C l a s s ) ;
4
5
CPPUNIT_TEST ( test_somma ) ;
6
CPPUNIT_TEST ( t e s t _ d i f f e r e n z a ) ;
7
CPPUNIT_TEST ( t e s t _ p r o d o t t o ) ;
14
2. Il Testing Automatico
8
CPPUNIT_TEST ( t e s t _ r a p p o r t o ) ;
9
10
CPPUNIT_TEST_SUITE_END ( ) ;
11
12
public :
13
...
14
15
};
Prossimo ed ultimo passo è quello di configurare l’ambiente per far sì che la
console di compilazione dell’IDE mostri i risultati dei test progettati.
1
int main ( )
2
{
3
CPPUNIT_NS : : T e s t R e s u l t r e s u l t ;
4
CPPUNIT_NS : : T e s t R e s u l t C o l l e c t o r c o l l e c t e d r e s u l t s ;
5
r e s u l t . a d d L i s t e n e r (& c o l l e c t e d r e s u l t s ) ;
6
7
CPPUNIT_NS : : B r i e f T e s t P r o g r e s s L i s t e n e r p r o g r e s s ;
8
r e s u l t . a d d L i s t e n e r (& p r o g r e s s ) ;
9
10
CPPUNIT_NS : : TestRunner runner ;
11
runner . addTest (
12
CPPUNIT_NS : : T e s t F a c t o r y R e g i s t r y : : g e t R e g i s t r y ( ) . makeTest ( )
13
);
14
runner . run ( r e s u l t ) ;
15
16
CPPUNIT_NS : : C o m p i l e r O u t p u t t e r c o m p i l e r o u t p u t (
17
&c o l l e c t e d r e s u l t s , std : : cerr
18
);
19
compileroutput . write ( ) ;
20
21
22
return c o l l e c t e d r e s u l t s . w a s S u c c e s s f u l ( ) ? 0 : 1 ;
}
Nelle due figure successive, sono riportati risultati di possibili esecuzioni del
caso in esame. In particolare, è mostrato come il fallimento della divisione con
dividendo nullo lanci un messaggio relativo ad un comportamento inaspettato.
L’esecuzione della test suite può quindi presentare tre risultati:
Successo : Se il risultato dei test dimostra assenza di malfunzionamenti. In tal
caso, però, il collaudo ha esito negativo.
Fallimento : Se l’esecuzione del test evidenzia malfunzionamenti.
15
2. Il Testing Automatico
Figura 2.2: Successo del test su OperazioniBase
Figura 2.3: Fallimento del test su OperazioniBase
Errore : Se il compilatore individua errori in fase di compilazione. Ciò significa
che il malfunzionamento risiede nel codice sorgente del collaudo.
2.3
Control Flow Graph
Il Control Flow Graph è un elemento base per il collaudo white box e rappresenta, in forma di grafo, tutti i possibili percorsi nel codice attraversabili durante l’esecuzione di un programma. Ogni nodo del grafo rappresenta un set di
istruzioni in cui non compaiono salti (jump).
16
2. Il Testing Automatico
Un CFG è formato da due blocchi principali che sono l’entry block e lo exit
block, da cui rispettivamente inizia e termina il grafo. Esisono inoltre due blocchi speciali: il back edge, che punta ad un blocco già attraversato, e lo abnormal
edge, che è un arco creato in presenza di handler per gestione di eccezioni.
L’utilizzo di tale grafo risulta fondamentale nel collaudo dal momento in cui
per poter condurre un’analisi di copertura bisogna identificare tutti i possibili
cammini nel codice, un’attività talvolta onerosa se si pensa alla dinamicità e
complessità di grandi software.
2.3.1
Strumenti a supporto della generazione automatica di Control Flow Graph
Come accennato nel paragrafo precedente, l’utilizzo di un Control Flow Graph
risulta fondamentale per poter procedere ad un’analisi di copertura che garantisca assenza di ridondanza e nel contempo una visione completa dei possibili
cammini del programma.
Purtroppo, strumenti che offrono la generazione automatica di tale diagramma per programmi scritti in C/C++ sono davvero pochissimi.
Dopo una lunga ricerca, si è deciso di menzionare Understand, software
prodotto dalla SciTools e distribuito con licenza commerciale.
2.3.2
Understand - Source Code Analysis & Metrics
Understand è un ambiente commerciale multi-piattaforma e multi-linguaggio
sviluppato dalla SciTools che fornisce supporto riguardo testing e reverse engineering. É basato sulla comprensione del codice e consente principalmente di
calcolare metriche ed esplorare viste che rappresentano diverse caratteristiche
del sorgente di un programma.
Supporta numerosi linguaggi di programmazione, tra i quali anche il C++.
Tra le molte funzionalità di Understand è possibile effettuare l’analisi di
numerose metriche. Quelle più significative sono sicuramente il conteggio delle linee di codice, il numero ciclomatico, il massimo livello di annidamento
per strutture di controllo e la somma delle complessità ciclomatiche di funzioni
annidate.
In aggiunta alla semplice visualizzazione testuale, oltre che alla possibilità di
esportazione dei risultati prodotti in formato CSV o HTML, Understand permet-
17
2. Il Testing Automatico
te di ottenere una rappresentazione grafica di tali metriche. Tale generazione è
molto semplice, ed è possibile grazie ai comandi presenti nell’ambiente sotto il
menu Metrics.
Understand offre davvero una vasta gamma di rappresentazioni grafiche
delle caratteristiche della classe/routine che si vuole analizzare e proprio tra
queste si collocano la generazione del CFG, del diagramma delle classi e delle
dipendenze.
2.3.3 Analisi di OperazioniBase attraverso Understand
Una volta creato il progetto e aggiunti i files da analizzare, basta scegliere la
routine da analizzare e scegliere la voce relativa al diagramma desiderato sotto
il menu Graphical Views.
Si è scelto di generare i diagrammi di chiamata e di controllo di flusso
offerti da Understand per il metodo test_rapporto. Gli output sono mostrati
nelle due figure seguenti:
Figura 2.4: Calls Graph per routine test_rapporto
Il primo diagramma, mostra la routine sottoposta ad analisi e le chiamate
effettuate nell’ordine sequenziale. La prima istruzione è proprio la creazione di
un’istanza della classe OperazioniBase. Segue la chiamata alla macro di assert di
CppUnit che invoca il metodo Rapporto dell’oggetto istanziato per effettuare il
confronto. L’istanza della classe viene, infine, eliminata attraverso il distruttore.
Il Control Flow Graph invece (fig. 2.5) è generato con le regole standard di
un diagramma di controllo di flusso. Ogni istruzione è rappresentata da un
blocco rettangolare contenente il testo dell’istruzione e avente contorno blu.
In presenza di un salto condizionato, Understand produce, seguendo le regole
UML un blocco contenente la condizione e gli eventuali collegamenti alle pos18
2. Il Testing Automatico
Figura 2.5: Control Flow Graph per routine test_rapporto
sibili scelte dell’utente/sistema. Nell’esempio è mostrato come la routine possa
lanciare il messaggio d’eccezione o proseguire con l’assert di verifica di CppUnit
a seconda del valore del dividendo (variabile b).
2.4
Analisi di Copertura
Come descritto nei paragrafi precedenti, esistono diverse misure per valutare
la bontà di un software. Il testing d’unità è una valida soluzione per affrontare
l’attività di collaudo mirata ad una verifica modulare, mentre il CFG risulta uno
strumento fondamentale per la progettazione di validi test cases e la verifica di
eventuali ridondanze nel codice attraverso un grafo. Un punto fondamentale
del processo di testing è però l’analisi di copertura, chiamata anche code coverage; che permette di stimare la quantità di codice effettivamente sottoposta
a collaudo. Sebbene possa sembrare un’attività alquanto semplice, l’analisi di
copertura nasconde peculiarità legate all’evoluzione dei moderni linguaggi di
programmazione. Basta pensare, infatti, alle proprietà di polimorfismo, eredi-
19
2. Il Testing Automatico
tarietà, alla dinamicità di un software, per dedurre che tale processo di verifica
vada utilizzato con attenzione.
2.4.1
Strumenti a supporto di Code Coverage automatico
Nonostante sul mercato siano presenti alternative di diversa natura per l’analisi
di copertura, ci si soffermerà principalmente su uno strumento Open Source
integrato in Eclipse. Il suo nome è gcov, e permette di ottenere un’analisi di
copertura del codice in maniera efficiente e veloce. Tuttavia, esistono numerose
alternative, tra le quali meritano menzione:
Test well CTC++ : É uno strumento commerciale volto all’analisi dinamica e
code coverage per linguaggi C/C++. Grazie all’analisi dinamica permette
la visualizzazione dei contatori di esecuzione del codice, misurando inoltre i costi (in termini temporali) di esecuzione delle funzioni, abilitando
il tracciamento di ingressi e uscite dalle stesse. Non richiede modifiche al
codice sorgente né compilazione di script. Offre inoltre la possibilità di
esportare i report dell’attività in file Excel tramite l’utility ctc2excel.
COVTOOL : Distribuito con licenza Open Source, COVTOOL è uno strumento che permette di effettuare l’analisi di copertura del codice a tempo
di compilazione. Tiene traccia delle linee di codice attraversate durante
l’esecuzione e produce un file di log che memorizza i dettagli dell’esecuzione. Esecuzioni multiple producono inoltre file di log multipli, e ciò è
un vantaggio se si vuole tener traccia della copertura di una test suite
piuttosto che di un singolo test case.
VectorCAST/Cover : Strumento commerciale che offre analisi di copertura e
salvataggio di report in diversi formati. Permette di testare una porzione
di codice o l’intero progetto fornendo anche un grafico rappresentate i
cammini coperti e quelli non coperti.
LCOV : Più che un tool vero e proprio è un’estensione da associare a gcov
per la produzione di output in formato HTML. É formato da una serie
di script PERL che consentono di leggere i risultati del test di copertura
e interpretarli producendo report ben strutturati. Supporta progetti di
grandi dimensioni ed è distribuito con licenza GPL e purtroppo disponibile
solo per ambieni Linux.
20
2. Il Testing Automatico
2.4.2
Analisi di Copertura con gcov
GCOV è un plugin Open Source integrato in Eclipse e distribuito con licenza EPL.
Consente di effettuare l’analisi di copertura del codice sorgente semplicemente lanciando il comando build dell’IDE e mandando in esecuzione il progetto.
Una caratteristica interessante del plugin è che permette di visualizzare in un
unico report le informazioni di copertura del progetto nella sua interezza, offrendo però allo sviluppatore la possibilità di scegliere in dettaglio quale classe
monitorare.
Per consentire Gcov di effettuare il suo lavoro, bisogna configurare le proprietà del progetto che stiamo collaudando, aggiungendo la direttiva
”-fprofile-arcs -ftest-coverage -std=c99”
alle sezioni GCC C++ Compiler/Miscellaneous e MinGW C++ Linker/Miscellaneous.
Completata la compilazione, gcov produce tanti file .gcno quanti sono i .cpp
del progetto, ognuno contenete informazioni di code coverage relative al modulo compilato. Nelle figure successive sono mostrati gli output di gcov relativi
ai test d’unità per la classe OperazioniBase.
Come si evince dall’output prodotto da gcov, il grado di copertura non è
totale dal momento in cui un’istruzione non viene eseguita con i dati di input
forniti per tale dimostrazione.
21
2. Il Testing Automatico
Figura 2.6: Output per analisi di copertura su OperazioniBase
22
Conclusioni
É ormai un dato di fatto che il processo di collaudo sia un vero e proprio fattore critico nel ciclo di sviluppo del software. Sempre più aziende, infatti, riconoscono l’esigenza di produrre software di qualità accostando alla progettazione ed allo sviluppo un processo di testing mirato ad effettuare un’analisi,
una rilevazione ed una rimozione degli effetti indesiderati in maniera veloce ed
efficace.
Dal momento in cui tale esigenza cresce molto velocemente, è bene valutare la possibilità di sfruttare strumenti che offrano l’opportunità di ridurre lo
sforzo umano automatizzando alcune delle attività che sono parte del processo
di testing.
Sono davvero tanti gli strumenti che permettono un’analisi veloce ed efficiente e ci si augura che nel prossimo futuro le potenzialità di tali strumenti
continuino la loro crescita.
23
Bibliografia
[1] [Standard ANSI/IEEE 1059-1993]
https://standards.ieee.org/findstds/standard/1059-1993.html
[2] [Ingegneria del Software 8a Edizione, Ian Sommerville, 2007.
Cap. 22-24]
[3] [Automazione del Testing ed Analisi Mutazionale]
http://www.federica.unina.it/ingegneria/ingegneria-softwareii/automazione-testing-analisi-mutazionale/
[4] [Documentazione CppUnit]
[http://sourceforge.net/apps/mediawiki/cppunit/index.php?title=Main_Page]
[5] [Documentazione Understand]
[http://www.scitools.com/features/metrics.php]
[6] [Documentazione gcov
[http://gcc.gnu.org/onlinedocs/gcc/Gcov.html]
24