6
Il test del software orientato agli oggetti deve prima di tutto tenere conto dei nuovi
ruoli e delle nuove relazioni che possono instaurarsi tra le entità presenti nel sistema
software. Inoltre, con l’aumentare della complessità della struttura del software e con
l’introduzione di nuovi costrutti nei linguaggi di programmazione, il software orientato
agli oggetti introduce nuove classi di difetti per le quali specifiche tecniche di test
devono essere sviluppate.
Nell’ambito specifico del test di unità e di integrazione, diventa rilevante la
verifica dei comportamenti di un oggetto nei diversi stati in cui esso può venirsi a
trovare. Il comportamento derivante dall’invocazione di un metodo dipende
direttamente dallo stato dell’oggetto su cui il metodo è invocato. Lo stato di un oggetto
ad un dato istante è determinato dai messaggi ricevuti dall’oggetto fino a quell’istante.
Inoltre, nel caso in cui l’oggetto si trovi in relazione con istanze di altre classi, il suo
comportamento è influenzato dallo stato e dai comportamenti degli altri oggetti e dalla
natura delle relazioni che li legano.
Difetti legati all’aggiornamento o all’uso dello stato di un oggetto possono essere
rilevati solamente attraverso l’invocazione di specifiche sequenze di metodi. Il test di
unità e di integrazione del software orientato agli oggetti deve prevedere quindi
l’identificazione delle sequenze di messaggi la cui esecuzione possa evidenziare
l’esistenza di questo tipo di difetti, per quanto riguarda il comportamento dell’oggetto
sia nel caso isolato (analisi intra-classe) sia nel caso la classe esaminata appartenga ad
un cluster (test inter-classe).
Al crescere della complessità del sistema software oggetto del test, la generazione
manuale dei casi di test può diventare impraticabile. Per questo motivo la ricerca si è
orientata verso lo sviluppo di tecniche che possano automatizzare, in tutto o in parte,
questa attività. In letteratura esistono diverse proposte di tecniche per la generazione
automatica di casi di test per il software orientato agli oggetti: solo in pochi casi però
queste proposte hanno portato allo sviluppo di tecniche completamente
automatizzabili.
In questo contesto si inserisce l’attività realizzata nel lavoro presentato in questa
tesi. Esso si sviluppa a partire dall’analisi di una tecnica per la generazione automatica
di casi di test per il test di unità del software orientato agli oggetti. Viene poi
7
presentata un’estensione della tecnica per supportare l’analisi inter-classe e la
generazione dei casi di test per il test di integrazione di cluster di classi, tra le quali
sussistano relazioni di aggregazione e dipendenza. Infine viene illustrata l’attività
sperimentale, che, attraverso l’utilizzo di un prototipo, si pone l’obiettivo di valutare
l’efficacia sia della tecnica esaminata, sia dell’estensione inter-classe proposta.
La tecnica intra-classe esaminata integra analisi del flusso dei dati, esecuzione
simbolica e deduzione automatica allo scopo di identificare sequenze di invocazioni
dei metodi di una classe tali da evidenziarne difetti legati all’aggiornamento o all’uso
dello stato dell’oggetto.
La tecnica inter-classe proposta estende la tecnica originale, adattando
opportunamente le fasi di analisi del flusso dei dati e di esecuzione simbolica, per
supportare la generazione dei casi di test anche nel caso in cui la classe esaminata sia
in relazione con altre classi. La tecnica prevede l’analisi progressiva delle classi che
compongono il cluster in esame, tenendo traccia dei risultati ottenuti a supporto
dell’analisi delle classi che da esse dipendono.
L’attività sperimentale ha previsto lo sviluppo di un prototipo che implementa il
processo di generazione proposto dalla tecnica intra- e inter-classe, integrando ed
estendendo strumenti per l’analisi del flusso dei dati e per l’esecuzione simbolica
sviluppati nell’ambito di altri progetti di ricerca.
La valutazione dei risultati ottenuti sottoponendo al prototipo opportuni benchmark
ha evidenziato positivamente le qualità della tecnica, per quanto riguarda sia
l’automatizzabilità del processo di generazione esaminato, sia la scalabilità della
tecnica inter-classe proposta.
La tesi è organizzata come segue.
• Nel capitolo 2 viene preso in esame il paradigma di sviluppo del software
orientato agli oggetti dal punto di vista delle problematiche che questa
tecnologia introduce in fase di collaudo.
8
• Nel capitolo 3 viene analizzato il problema della generazione automatica dei
casi di test per il software orientato agli oggetti, ponendo particolare
attenzione alle diverse soluzioni proposte nelle tecniche esistenti. Tali
tecniche vengono analizzate e confrontate sulla base del reale grado di
automatizzabilità ottenibile da una loro implementazione.
• Nel capitolo 4 viene presentata la tecnica intra-classe oggetto della
sperimentazione. In questo capitolo viene introdotto il processo di
generazione proposto, analizzando in dettaglio le diverse fasi che lo
compongono, accompagnando la spiegazione con l’applicazione del
processo ad un caso di studio.
• Nel capitolo 5 viene presentata l’estensione della tecnica per l’analisi inter-
classe. La tecnica estesa permette di generare casi di test per una classe in
relazione di aggregazione o dipendenza con altre classi. L’obiettivo della
tecnica sviluppata, da verificare attraverso l’attività sperimentale, consiste
nell’ottenere un metodo di analisi scalabile, le cui prestazioni non dipendano
dal numero delle classi del cluster e dalle relazioni che le legano.
• Nel capitolo 6 viene descritto il prototipo sviluppato a supporto dell’attività
sperimentale. Il prototipo implementa quasi interamente il processo di
generazione proposto dalla tecnica di test intra-classe e inter-classe. Il
capitolo descrive prima di tutto l’architettura dello strumento sviluppato,
esaminando poi in dettaglio il ruolo e le funzionalità dei singoli componenti
che ne fanno parte.
• Nel capitolo 7 vengono presentati i risultati dell’attività vera e propria di
sperimentazione. Tale attività ha comportato l’applicazione dello strumento a
software di benchmark appositamente sviluppati e la valutazione dei risultati
ottenuti. Tali risultati vengono analizzati in dettaglio per evidenziare
caratteristiche sia della tecnica sia del prototipo sviluppato.
• Infine, nel capitolo 8 vengono tratte le conclusioni del lavoro svolto. La
valutazione sull’effettiva automatizzabilità della tecnica è pienamente
positiva, avendo ottenuto uno strumento in grado di generare in modo
completamente autonomo i casi di test per il test di una classe. Anche dal
9
punto di vista della scalabilità nell’analisi interclasse i risultati sono più che
soddisfacenti.
• In appendice, vengono forniti dettagli sull’implementazione del prototipo,
presentando l’architettura e le interfacce software dei componenti integrati
nello strumento.
10
Capitolo 2.
Il test del software
orientato agli oggetti
L’utilizzo di metodologie di progetto orientate agli oggetti è oggi diffuso in quasi tutte
le aree dello sviluppo del software. Per poter assicurare la qualità del software
prodotto, la fase di collaudo non può ignorare le peculiarità introdotte con
l’approccio orientato agli oggetti. Il nuovo approccio allo sviluppo, con l’introduzione
di nuovi costrutti nei linguaggi di programmazione, causa il nascere di nuove classi di
difetti che le tecniche tradizionali non sono in grado di rilevare.
In questo capitolo vengono esaminate le caratteristiche del software orientato agli
oggetti, ponendo l’attenzione su quali sono gli effetti sul collaudo del software e in che
modo esse possono introdurre nuove classi di difetti nel software sviluppato.
11
All’interno dello studio dell’approccio orientato agli oggetti (OO), il test ha ricevuto
una minore attenzione rispetto all’analisi, al progetto e allo sviluppo. Il minor interesse
è stato principalmente causato dalla convinzione iniziale che il metodo di sviluppo
orientato agli oggetti potesse assicurare la qualità del codice prodotto, senza la
necessità di un test adeguato. Una corretta applicazione dei dettami della progettazione
orientata agli oggetti permette di ottenere software di qualità, secondo metriche
riguardanti la modularità, l’incapsulamento o la riusabilità, ma questo non impedisce
in alcun modo la presenza di difetti nel codice prodotto.
L’attività dello sviluppo del software orientato agli oggetti non è meno soggetta ad
errori di quanto lo sia per il software tradizionale; inoltre, il software OO presenta
peculiarità, sia nella metodologia di sviluppo sia nei costrutti propri dei linguaggi
utilizzati, tali da introdurre nuove categorie di difetti e da richiedere apposite tecniche
per un’appropriata analisi e per un efficace collaudo del software.
L’attività di test assume inoltre un’importanza maggiore se interpretata nel
contesto di riuso introdotto dal paradigma orientato agli oggetti: un componente
software può essere riusato solo se si è ragionevolmente sicuri che il suo codice non
presenti difetti.
Nei paragrafi successivi vengono prese in esame le caratteristiche del software
orientato agli oggetti che influenzano maggiormente l’attività di test, sia perché
introducono nuove classi di difetti, sia perché aumentano la complessità della struttura
del sistema in esame, rendendo in molti casi inapplicabili le tecniche di test
tradizionali.
Per quanto riguarda la genericità e la gestione delle eccezioni, esse non sono
elementi peculiari del paradigma di sviluppo orientato agli oggetti, in quanto presenti
anche in altri linguaggi di programmazione. Vengono qui prese in considerazione in
quanto in genere i sistemi software orientati agli oggetti fanno largo uso sia del
meccanismo di gestione delle eccezioni sia, dove il linguaggio di programmazione lo
consenta, della genericità. Sono anche questi quindi degli elementi che non possono
essere ignorati in fase di analisi e test del software orientato agli oggetti.
12
Stato e information hiding
Nei sistemi software tradizionali l’unità minima oggetto del test è costituita dai
sottoprogrammi. Questo non accade però nel test del software orientato agli oggetti: i
metodi di una classe sono semanticamente e strutturalmente in relazione tra loro e per
questo non possono essere testati come entità isolate.
Nei sistemi software orientati agli oggetti viene introdotto il concetto di stato di
una classe, determinato dai valori assunti dagli attributi dell’oggetto (detti variabili di
stato) in un certo istante dell’esecuzione.
L’invocazione di un metodo può modificare lo stato dell’oggetto in quanto
definisce uno o più attributi. Allo stesso tempo, l’esecuzione del metodo può essere
influenzata dallo stato dell’oggetto in quanto può dipendere dal valore degli attributi.
L’incapsulazione e l’information hiding introducono ulteriori difficoltà in fase di
test in quanto lo stato di un oggetto non è esplicito, accessibile e visibile all’esterno.
L’unico modo per accedere allo stato di un oggetto è l’invocazione di un metodo, che
costituisce però di per sé un filtro a ciò che accade veramente allo stato interno.
In questa situazione le tecniche tradizionali si rivelano essere insufficienti ad
esercitare efficacemente il sistema software in esame: il test di un singolo metodo non
permette di evidenziare comportamenti legati allo stato dell’oggetto, determinato dalle
invocazioni precedentemente avvenute. Diventano rilevanti le sequenze di invocazioni
dei metodi della classe: una sequenza di invocazioni, sotto certe condizioni sui
parametri di chiamata, porta l’istanza in uno stato determinato e prevedibile.
Sfruttando questo fatto è possibile cercare di testare il comportamento della classe nei
diversi stati in cui si può venire a trovare durante l’interazione con gli altri componenti
del sistema software.
13
Shadow invocations
Si tratta di metodi eseguiti senza che nel codice sia presente una loro esplicita
invocazione: si pensi ai metodi costruttori e distruttori, invocati ogni volta che
un’istanza della classe viene allocata o deallocata.
Lo stesso accade nel caso in cui il linguaggio utilizzato supporti l’overloading
degli operatori: espressioni che in un linguaggio tradizionale sono facilmente testabili
in quanto operazioni elementari, per un linguaggio orientato agli oggetti possono
costituire l’invocazione di un metodo.
Nelle tecniche tradizionali il problema non si pone, mentre le tecniche di test per il
software orientato agli oggetti devono tener conto anche di questi comportamenti per
poter rilevare eventuali difetti legati alle invocazioni non esplicite dei metodi.
Binding dinamico e polimorfismo
Il termine polimorfismo si riferisce alla possibilità per un’entità di cambiare il proprio
tipo durante l’esecuzione del software. In C++ e Java il tipo che l’entità può assumere
a run-time è vincolato dalla gerarchia dei tipi: un’entità staticamente dichiarata di tipo
A può dinamicamente diventare di qualunque tipo B, tale che B sia A o un sottotipo di
A.
Al polimorfismo si accompagna il binding dinamico: il metodo effettivamente
chiamato in conseguenza di un’invocazione su un’entità polimorfa dipende dal tipo
dell’entità al momento dell’invocazione. In generale non è possibile stabilire
staticamente quale sarà questo metodo. Inoltre, l’invocazione può comportare il
passaggio di parametri attuali che possono essere a loro volta dei riferimenti
polimorfici.
Un difetto nella gestione di un riferimento polimorfico può portare all’invocazione
di un metodo sull’oggetto sbagliato della gerarchia: la ridefinizione di un metodo può
cambiarne radicalmente la semantica, introducendo, nel caso di un’invocazione non
desiderata, dei malfunzionamenti.
14
Anche in questo caso, per poter gestire correttamente la presenza di riferimenti
polimorfici tra gli oggetti e per poter efficacemente rilevare le nuove classi di difetti
introdotte, è necessario fare ricorso a tecniche appositamente sviluppate ([ALO99],
[ORS98]).
Conversioni di tipo
Per conversione di tipo s’intende un’operazione, chiamata spesso casting, che
permette di modificare dinamicamente il tipo associato ad un’entità di programma. Gli
stessi compilatori spesso non possono compiere alcun controllo sulle conversioni di
tipo, che in caso di errori introducono comportamenti imprevisti in fase di esecuzione.
I difetti più critici legati alle conversioni di tipo coinvolgono in genere anche
riferimenti polimorfici: un’entità a cui è stato modificato dinamicamente il tipo in
modo errato si comporta in modo imprevisto al momento dell’invocazione di un
metodo. Per poter individuare questi difetti è necessario stabilire strategie di test
dedicate, che prevedano la costruzione dell’opportuno scaffolding e l’utilizzo di
strumenti per l’analisi dinamica del comportamento del software.
Ereditarietà
In un linguaggio tradizionale le unità oggetto del test, che come si è già detto sono i
sottoprogrammi, non sono in alcuna relazione tra loro, se non per il fatto che possono
contenere invocazioni innestate. Ciò influenza però solo l’ordine con cui essi sono
testati e non l’attività specifica del test di unità.
In un linguaggio OO, l’ereditarietà è un meccanismo che permette lo sviluppo di
nuove classi a partire dall’implementazione di classi esistenti, ridefinendo alcuni
metodi, introducendone altri o aggiungendo variabili di stato.
Si può osservare prima di tutto che il comportamento dei metodi della classe non è
interamente esplicitato nell’ambito della classe, ma deriva dalla sovrapposizione delle
definizioni di tutte le classi che la precedono nella gerarchia.
15
Ad un primo esame si può pensare che il sussistere di una relazione di questo tipo
possa portare solo vantaggi all’attività di test, permettendo di evitare di ripetere il
collaudo dei metodi non ridefiniti. Questo non ha però senso poiché i metodi non
vengono testati singolarmente ed è necessario quindi inquadrare la ridefinizione dei
metodi nel complesso della semantica della classe.
La relazione di ereditarietà può essere effettivamente sfruttata, ma questo consiste
nell’evitare di ripetere il collaudo delle sequenze che coinvolgono esclusivamente
metodi non ridefiniti. Un ulteriore vantaggio può sorgere dal fatto che, se la sottoclasse
mantiene la semantica dei metodi ridefiniti, i casi di test generati per la superclasse
possono rimanere validi anche per la sottoclasse.
In alcuni casi è possibile partizionare gli attributi in modo tale da identificare quelli
che riguardano solo lo stato della classe antenata e quindi influenzano e vengono
influenzati dall’esecuzione dei soli metodi della classe antenata. Per questi attributi
può non essere necessario ripetere la generazione dei casi di test.
Per quanto riguarda la relazione di ereditarietà, specifiche tecniche sono state
sviluppate a supporto sia del test di integrazione sia del test di unità ([HMK92],
[ORS98]).
Aggregazione, associazione e dipendenza
Il paradigma orientato agli oggetti, oltre al concetto di ereditarietà, introduce altre
relazioni, aggregazione, associazione e dipendenza, che possono instaurarsi tra le
classi del sistema software. Queste relazioni creano una fitta trama di legami tra le
classi: sia lo stato sia il comportamento di un oggetto dipendono dallo stato e dai
comportamenti delle classi con cui l’oggetto è in relazione.
La relazione di aggregazione è direttamente riconducibile alla relazione di
contenimento tra tipi: la classe A è in relazione di aggregazione con la classe B se uno
dei suoi attributi è un’istanza della classe B. Lo stato dell’istanza della classe B è parte
dello stato dell’istanza della classe A ed influenza direttamente il comportamento dei
metodi. Lo stato di B, supposto che tutti gli attributi di A siano privati, può essere
16
modificato solo come effetto di un’invocazione di un metodo di A, in quanto l’istanza
di B non è accessibile direttamente.
La relazione di associazione sussiste quando uno degli attributi della classe A è un
riferimento ad una istanza della classe B: la differenza rispetto all’aggregazione
consiste nel fatto che l’istanza della classe B ha un ciclo di vita indipendente
dall’istanza di A. Inoltre lo stato dell’istanza di B può essere modificato da interazioni
dirette, senza che venga invocato alcun metodo di A.
La relazione di dipendenza, infine, occorre quando un metodo della classe A usa, si
riferisce o crea un’istanza di B: il legame con la classe B sussiste in quanto uno dei
parametri del metodo, o il valore di ritorno, oppure una variabile locale, è un’istanza di
B o un suo riferimento.
Ciascuna delle relazioni di aggregazione, associazione e dipendenza, a causa delle
loro differente natura, ha un diverso impatto sull’attività di test inter-classe, sia per il
tipo di analisi svolta, sia per la classe di difetti che si vuole rilevare.
La relazione di aggregazione fa sì che lo stato della classe A sia il risultato
composito del valore dei suoi attributi e dello stato delle istanze delle classi con cui è
in relazione. Ogni elemento che compone lo stato è però comunque imputabile alla
storia dell’istanza, in termini dei metodi che sono stati invocati su di essa. Non è
possibile accedere infatti allo stato di un attributo strutturato se non attraverso
l’invocazione di un metodo. Questo naturalmente nell’ipotesi semplificativa in cui gli
attributi siano tutti privati; si tratta comunque di un’ipotesi ragionevole, in quanto, in
caso contrario, sarebbe comunque possibile definire degli appositi metodi di get/set su
ciascuno degli attributi e ricondursi alla condizione indicata.
La relazione di associazione è molto più complessa da gestire, in quanto, come è
stato detto, lo stato dell’istanza di cui si ha il riferimento può cambiare
indipendentemente dalla storia e dallo stato della classe in esame: viene introdotto in
questo modo un notevole aumento della complessità dell’analisi e del test del sistema
software e per questo sono molto poche le tecniche che supportano questo tipo di
relazione.
Per quanto riguarda la relazione di dipendenza, essa modellizza una situazione in
cui al momento dell’invocazione di un metodo e durante la sua esecuzione entra in
17
gioco un’istanza di un’altra classe o un suo riferimento, in forma di parametro, valore
di ritorno o variabile locale. La presenza e l’uso di questa istanza ha però solo effetti
locali e non vi è alcun legame diretto tra il suo stato e lo stato dell’oggetto in esame.
In conclusione, il sussistere di queste relazioni tra le classi del sistema collaudato
influenza direttamente l’attività di test in quanto:
- le classi del sistema devono essere testate rispettando un ordine di visita che
permetta di tener efficacemente conto delle relazioni presenti.
- Per poter attuare il test è necessario sviluppare dello scaffolding, che risulta
più complesso al crescere della complessità delle relazioni tra la classe in
esame e le altre classi del sistema.
- Le tecniche di generazione dei casi di test devono tenere conto dell’esistenza
di queste relazioni poiché anche l’individuazione delle altre classi di difetti
può risultare diversa quando l’oggetto del test non è più una classe isolata.
Genericità
Un linguaggio supporta la genericità se fornisce dei costrutti tali da permettere al
programmatore di definire dei tipi di dati astratti in modo parametrico, che devono
quindi essere istanziati, specificando i parametri, prima di poter essere usati.
La genericità viene qui presa in considerazione in quanto, pur non essendo
formalmente una caratteristica del paradigma orientato agli oggetti, la maggior parte
dei linguaggi OO la supportano. Inoltre la genericità è uno degli elementi chiave per la
costruzione di componenti riusabili.
Una classe generica non può essere testata se essa non viene prima istanziata,
specificandone i parametri. Per poter testare appropriatamente una classe generica è
necessario quindi individuare o definire uno o più tipi da passare come parametro alla
classe istanziata. Nessuna ipotesi può però essere fatta sui parametri che verranno usati
per l’istanziazione del tipo generico nel sistema testato; la scelta dei parametri da usare
in fase di test è critica per poter evidenziare specifici difetti della classe generica.
18
In questo contesto i parametri di istanziazione della classe generica usati in fase di
collaudo possono essere considerati come degli stub e devono essere quindi sviluppati
appositamente secondo una precisa strategia di test.
Gestione delle eccezioni
Il meccanismo delle eccezioni permette di interrompere il normale flusso di
programma per poter gestire appropriatamente una condizione di errore consentendo il
ripristino di uno stato corretto, la risoluzione della causa dell’errore oppure una
corretta terminazione dell’esecuzione.
La peculiarità introdotta dal meccanismo delle eccezioni consiste nell’asincronicità
con cui l’eccezione può essere sollevata, rendendo impraticabile la copertura di tutte le
eccezioni e di tutti i punti in cui possono essere sollevate.
La stessa eccezione può essere inoltre gestita in più parti del codice e durante la
sua gestione altre eccezioni possono essere sollevate. In molti casi risulta molto
difficoltoso sia prevedere il comportamento del sistema a seguito dell’insorgere di una
eccezione, sia verificare il comportamento stesso. Anche in questo caso il test di questi
aspetti del sistema comporta l’analisi dinamica del software e per questo sono molto
poche le tecniche che si propongono di individuare difetti legati alla gestione delle
eccezioni.
Sulla base di quanto è stato detto finora, è evidente che, per poter sviluppare una
tecnica efficace per il test del software orientato agli oggetti, è necessario spesso
focalizzarsi su un aspetto ben preciso del problema; l’integrazione dei risultati
dell’applicazione di diverse tecniche specifiche permette poi di realizzare un’attività di
collaudo che copra con efficacia tutti gli aspetti peculiari del software orientato agli
oggetti.