Strumenti a supporto del testing automatico di software sviluppato in
by user
Comments
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