Sei sulla pagina 1di 34

11.

1 Concetti di test
In questa sezione, presentiamo gli elementi del modello utilizzati durante i test (Figura 11-2):

• Un test component (componente di test) è una parte del sistema che può essere isolata
per i test. Un componente può essere un oggetto, un gruppo di oggetti o uno o più
sottosistemi.
• Un fault (guasto), chiamato anche bug o difetto, è un errore di progettazione o di codifica
che può causare un comportamento anomalo del componente.
• Uno erroneous state (stato errato) è una manifestazione di un errore durante l'esecuzione
del sistema. Uno stato errato è causato da uno o più guasti e può portare a un fallimento.
• Un failure (fallimento) è una deviazione tra la specifica e il comportamento effettivo. Un
fallimento è innescato da uno o più stati errati. Non tutti gli stati errati innescano un
fallimento. 2
• Un test case (caso di test) è un insieme di input e risultati attesi che esercita un
componente di test con lo scopo di provocare guasti e rilevare errori.
• Uno test pub (stub di test) è un'implementazione parziale di componenti da cui dipende il
componente testato. Un driver di test è un'implementazione parziale di un componente
che dipende dal componente di test. Gli stub e i driver di test permettono di isolare i
componenti dal resto del sistema per i test.

1. Si noti che, al di fuori della comunità dei test, gli sviluppatori spesso non distinguono tra
errori, fallimenti e stati errati, e invece si riferiscono a tutti e tre i concetti come
"errori".
• Una correction (correzione) è una modifica di un componente. Lo scopo di una correzione
è di riparare un guasto. Si noti che una correzione può introdurre nuovi difetti.

11.3.1 Guasti, stati erronei e fallimenti


Con la comprensione iniziale dei termini dalle definizioni della Sezione 11.3, diamo
un'occhiata alla Figura 11-3. Cosa vedete? La Figura 11-3 mostra una coppia di binari che
non sono allineati tra loro. Se immaginiamo un treno che corre sui binari, si schianterebbe
(fallisce). Tuttavia, la figura in realtà non presenta un fallimento, né uno stato errato, né un
guasto. Non mostra un fallimento, perché il comportamento atteso non è stato specificato,
né c'è alcun comportamento osservato. Anche la figura 11-3 non mostra uno stato errato,
perché ciò significherebbe che il sistema è in uno stato in cui ulteriori elaborazioni
porterebbero a un fallimento. Qui vediamo solo binari; non viene mostrato nessun treno in
movimento. Per parlare di stato errato, fallimento o errore, abbiamo bisogno di
confrontare il comportamento desiderato (descritto nel caso d'uso nel RAD) con il
comportamento osservato (descritto dal caso di test). Supponiamo di avere un caso d'uso
con un treno che si muove dal binario superiore sinistro al binario inferiore destro (Figura
11-4).

Possiamo quindi procedere a derivare un caso di test che sposta il treno dallo stato
descritto nella condizione di entrata del caso d'uso a uno stato in cui si schianterà, cioè
quando sta lasciando il binario superiore (Figura 11-5).
In altre parole, quando si esegue questo caso di test, possiamo dimostrare che il sistema
contiene un guasto. Notate che lo stato attuale mostrato nella Figura 11-6 è errato, ma non
mostra un guasto.
Il disallineamento dei binari può essere il risultato di una cattiva comunicazione tra i team
di sviluppo (ogni binario doveva essere posizionato da un team) o a causa di un'errata
implementazione delle specifiche da parte di uno dei team (Figura 11-7). Entrambi sono
esempi di difetti algoritmici. Probabilmente avete già familiarità con molti altri difetti
algoritmici che vengono introdotti durante la fase di implementazione. Per esempio,
"uscire da un ciclo troppo presto", "uscire da un ciclo troppo tardi", "testare la condizione
sbagliata", "dimenticare di inizializzare una variabile" sono tutti errori algoritmici specifici
dell'implementazione. Gli errori algoritmici possono verificarsi anche durante l'analisi e la
progettazione del sistema. I problemi di stress e di sovraccarico, per esempio, sono guasti
algoritmici specifici della progettazione dell'oggetto che portano al fallimento quando le
strutture dati sono riempite oltre la loro capacità specificata. I fallimenti del throughput e
delle prestazioni sono possibili quando un sistema non funziona alla velocità specificata dai
requisiti non funzionali.
Anche se i binari sono implementati secondo le specifiche del RAD, potrebbero comunque
finire disallineati durante il funzionamento quotidiano, per esempio, se accade un
terremoto che sposta il terreno sottostante (Figura 11-8).
Un errore nella macchina virtuale di un sistema software è un altro esempio di errore
meccanico: anche se gli sviluppatori hanno implementato correttamente, cioè hanno
mappato correttamente il modello a oggetti nel codice, il comportamento osservato può
ancora deviare dal comportamento specificato. Nei progetti di concurrent engineering, per
esempio, dove l'hardware è sviluppato in parallelo al software, non possiamo sempre dare
per scontato che la macchina virtuale venga eseguita come specificato. Altri esempi di
guasti meccanici sono le interruzioni di corrente. Si noti la relatività dei termini "guasto" e
"fallimento" rispetto a un particolare componente del sistema: il guasto in un componente
del sistema (il sistema di alimentazione) è il guasto meccanico che può portare al fallimento
di un altro componente del sistema (il sistema software).

11.3.2 Casi di test


Un caso di test è un insieme di dati di input e risultati attesi che esercita un componente
con lo scopo di provocare fallimenti e rilevare guasti. Un caso di test ha cinque attributi:
nome, posizione, input, oracolo e log (Tabella 11-1). Il nome del caso di test permette al
tester di distinguere tra diversi casi di test. Un'euristica per nominare i casi di test è di
derivare il nome dal requisito che si sta testando o dal componente da testare. Per
esempio, se si sta testando un caso d'uso Deposito(), si potrebbe chiamare il caso di test
Test_Deposit. Se un caso di test coinvolge due componenti A e B, un buon nome sarebbe
Test_AB. L'attributo location descrive dove il test case può essere trovato. Dovrebbe essere
il nome del percorso o l'URL dell'eseguibile del programma di test e dei suoi input.
L'input descrive l'insieme dei dati di input o dei comandi che devono essere inseriti
dall'attore del caso di test (che può essere il tester o un test driver). Il comportamento
atteso del caso di test è la sequenza di dati o comandi in uscita che una corretta esecuzione
del test dovrebbe produrre. Il comportamento atteso è descritto dall'attributo oracolo. Il
log è un insieme di correlazioni temporali del comportamento osservato con il
comportamento atteso per varie esecuzioni del test.
Una volta che i casi di test sono identificati e descritti, si identificano le relazioni tra i casi di
test. L'aggregazione e le associazioni precedenti sono usate per descrivere le relazioni tra i
casi di test. L'aggregazione è usata quando un caso di test può essere scomposto in un
insieme di sottotest. Due casi di test sono collegati tramite l'associazione precede quando
un caso di test deve precedere un altro caso di test.
La Figura 11-9 mostra un modello di test in cui TestA deve precedere TestB e TestC. Per
esempio, TestA consiste di TestA1 e TestA2, il che significa che una volta che TestA1 e
TestA2 sono testati, TestA è testato; non c'è un test separato per TestA. Un buon modello
di test ha meno associazioni possibili, perché i test che non sono associati tra loro possono
essere eseguiti indipendentemente l'uno dall'altro. Questo permette ad un tester di
velocizzare i test, se le risorse di test necessarie sono disponibili. Nella Figura 11-9, TestB e
TestC possono essere testati in parallelo, perché non c'è alcuna relazione tra loro.
I casi di test sono classificati in test blackbox e test whitebox, a seconda di quale aspetto del
modello di sistema viene testato. I test blackbox si concentrano sul comportamento di
input/output del componente. I test blackbox non si occupano degli aspetti interni del
componente, né del comportamento o della struttura dei componenti. I test whitebox si
concentrano sulla struttura interna del componente. Un test whitebox si assicura che,
indipendentemente dal particolare comportamento di input/output, ogni stato nel modello
dinamico dell'oggetto e ogni interazione tra gli oggetti sia testato. Di conseguenza, il test
whitebox va oltre il test blackbox. Infatti, la maggior parte dei test whitebox richiedono dati
di input che non potrebbero essere derivati da una descrizione dei soli requisiti funzionali. Il
test unitario combina entrambe le tecniche di test: il test blackbox per testare la
funzionalità del componente e il test whitebox per testare gli aspetti strutturali e dinamici
del componente.

11.3.3 Stub di test e driver


L'esecuzione di casi di test su componenti singoli o combinazioni di componenti richiede
che il componente testato sia isolato dal resto del sistema. I test driver e i test stub sono
usati per sostituire le parti mancanti del sistema. Un test driver simula la parte del sistema
che chiama il componente sotto test. Un test driver passa gli input di test identificati
nell'analisi del test case al componente e visualizza i risultati.
Uno stub di test simula un componente che viene chiamato dal componente testato. Lo
stub di test deve fornire la stessa API del metodo del componente simulato e deve
restituire un valore conforme al tipo di risultato di ritorno della firma del tipo del metodo.
Si noti che l'interfaccia di tutti i componenti deve essere baseline. Se l'interfaccia di un
componente cambia, anche i corrispondenti test driver e stub devono cambiare.
L'implementazione degli stub di test è un compito non banale. Non è sufficiente scrivere
uno stub di test che stampa semplicemente un messaggio che afferma che lo stub di test è
stato chiamato. Nella maggior parte delle situazioni, quando il componente A chiama il
componente B, A si aspetta che B esegua del lavoro, che viene poi restituito come un
insieme di parametri di risultato. Se lo stub di test non simula questo comportamento, A
fallirà, non a causa di un errore in A, ma perché lo stub di test non simula correttamente B.
Anche fornire un valore di ritorno non è sempre sufficiente. Per esempio, se uno stub di
test restituisce sempre lo stesso valore, potrebbe non restituire il valore atteso dal
componente chiamante in un particolare scenario. Questo può produrre risultati confusi e
persino portare al fallimento del componente chiamante, anche se è implementato
correttamente. Spesso, c'è un compromesso tra l'implementazione di stub di test accurati e
la sostituzione degli stub di test con il componente effettivo. Per molti componenti, i driver
e gli stub sono spesso scritti dopo che il componente è stato completato, e per i
componenti che sono in ritardo, gli stub spesso non sono scritti affatto.
Per assicurare che stub e driver siano sviluppati e disponibili quando necessario, diversi
metodi di sviluppo stabiliscono che i driver siano sviluppati per ogni componente. Questo si
traduce in uno sforzo minore perché fornisce agli sviluppatori l'opportunità di trovare
problemi con la specifica dell'interfaccia del componente in prova prima che sia
completamente implementato.
11.3.4 Correzioni
Una volta che i test sono stati eseguiti e i guasti sono stati rilevati, gli sviluppatori cambiano
il componente per eliminare i guasti sospetti. Una correzione è una modifica ad un
componente il cui scopo è quello di riparare un guasto. Le correzioni possono variare da
una semplice modifica ad un singolo componente, ad una completa riprogettazione di una
struttura dati o di un sottosistema. In tutti i casi, la probabilità che lo sviluppatore introduca
nuovi difetti nel componente revisionato è alta. Diverse tecniche possono essere usate per
minimizzare il verificarsi di tali difetti:

• Il problem tracking include la documentazione di ogni guasto, stato errato e difetto


rilevato, la sua correzione e le revisioni dei componenti coinvolti nel cambiamento. Insieme
alla gestione della configurazione, il problem tracking permette agli sviluppatori di
restringere la ricerca di nuovi guasti. Descriviamo il problem tracking in modo più
dettagliato nel Capitolo 13, Gestione della configurazione.
• Il test di regressione include la riesecuzione di tutti i test precedenti dopo un
cambiamento. Questo assicura che la funzionalità che funzionava prima della correzione
non sia stata influenzata. Il test di regressione è importante nei metodi orientati agli
oggetti, che richiedono un processo di sviluppo iterativo. Questo richiede che i test siano
iniziati prima e che le suite di test siano mantenute dopo ogni iterazione. I test di
regressione purtroppo sono costosi, specialmente quando una parte dei test non è
automatizzata. Descriviamo i test di regressione in modo più dettagliato nella Sezione
11.4.4.
• La manutenzione delle motivazioni include la documentazione della logica del
cambiamento e la sua relazione con la logica del componente rivisto. La manutenzione
delle motivazioni permette agli sviluppatori di evitare di introdurre nuovi difetti
controllando i presupposti che sono stati usati per costruire il componente. Descriviamo la
manutenzione delle motivazioni nel Capitolo 12, Gestione delle motivazioni.

In seguito, descriviamo più in dettaglio le attività di test che portano alla creazione di casi di
test, la loro esecuzione e lo sviluppo di correzioni.

11.2 Attività di test


In questa sezione, descriviamo le attività tecniche di test. Queste includono

• Ispezione dei componenti, che trova i difetti in un singolo componente attraverso


l'ispezione manuale del suo codice sorgente (Sezione 11.4.1)
• Test di usabilità, che trova le differenze tra ciò che il sistema fa e l'aspettativa degli
utenti su ciò che dovrebbe fare (Sezione 11.4.2)
• Il test unitario, che trova i difetti isolando un singolo componente usando stub e
driver di test ed esercitando il componente usando casi di test (Sezione 11.4.3)
• Test di integrazione, che trova i difetti integrando diversi componenti insieme
(sezione 11.4.4)
• Test di sistema, che si concentra sul sistema completo, i suoi requisiti funzionali e
non funzionali, e il suo ambiente di destinazione (Sezione 11.4.5).

11.4.1 Ispezione dei componenti


Le ispezioni trovano i difetti in un componente esaminando il suo codice sorgente in una
riunione formale. Le ispezioni possono essere condotte prima o dopo il test unitario. Il
primo processo di ispezione strutturato fu il metodo di ispezione di Michael Fagan [Fagan,
1976]. L'ispezione è condotta da un team di sviluppatori, incluso l'autore del componente,
un moderatore che facilita il processo, e uno o più revisori che trovano difetti nel
componente. Il metodo di ispezione di Fagan consiste in cinque passi:

• Panoramica. L'autore del componente presenta brevemente lo scopo e la portata


del componente e gli obiettivi dell'ispezione.
• Preparazione. I revisori diventano familiari con l'implementazione del componente.
• Riunione d'ispezione. Un lettore parafrasa il codice sorgente del componente, e il
team d'ispezione solleva problemi con il componente. Un moderatore mantiene la
riunione in pista.
• Rielaborazione. L'autore rivede il componente.
• Follow-up. Il moderatore controlla la qualità della rilavorazione e può determinare il
componente che deve essere ispezionato di nuovo.

I passi critici in questo processo sono la fase di preparazione e la riunione di ispezione.


Durante la fase di preparazione, i revisori diventano familiari con il codice sorgente; non si
concentrano ancora sulla ricerca di difetti. Durante l'incontro di ispezione, il lettore
parafrasa il codice sorgente, cioè legge ogni dichiarazione del codice sorgente e spiega cosa
dovrebbe fare la dichiarazione. I revisori poi sollevano problemi se pensano che ci sia un
errore. La maggior parte del tempo è spesa a discutere se un difetto è presente o meno, ma
le soluzioni per riparare il difetto non sono esplorate a questo punto. Durante la fase
generale dell'ispezione, l'autore dichiara gli obiettivi dell'ispezione. Oltre a trovare i difetti,
ai revisori può essere chiesto di cercare deviazioni dagli standard di codifica o inefficienze.
Le ispezioni di Fagan sono solitamente percepite come dispendiose in termini di tempo a
causa della lunghezza della fase di preparazione e della riunione di ispezione. L'efficacia di
una revisione dipende anche dalla preparazione dei revisori. David Parnas ha proposto un
processo di ispezione rivisto, l'active design review, che elimina la riunione di ispezione di
tutti i membri del team di ispezione [Parnas & Weiss, 1985]. Invece, ai revisori viene chiesto
di trovare i difetti durante la fase di preparazione. Alla fine della fase di preparazione, ogni
revisore compila un questionario che verifica la sua comprensione del componente.
L'autore poi incontra individualmente ogni revisore per raccogliere il feedback sul
componente. Sia le ispezioni di Fagan che le revisioni attive del progetto hanno dimostrato
di essere solitamente più efficaci dei test nello scoprire i guasti. Sia i test che le ispezioni
sono usati in progetti critici per la sicurezza, poiché tendono a trovare diversi tipi di guasti.

11.4.2 Test di usabilità


Il test di usabilità verifica la comprensione del sistema da parte dell'utente. Il test di
usabilità non confronta il sistema con una specifica. Invece, si concentra sul trovare le
differenze tra il sistema e l'aspettativa degli utenti su ciò che dovrebbe fare. Poiché è
difficile definire un modello formale dell'utente contro cui testare, i test di usabilità
adottano un approccio empirico: i partecipanti rappresentativi della popolazione di utenti
trovano i problemi manipolando l'interfaccia utente o una sua simulazione. I test di
usabilità riguardano anche i dettagli dell'interfaccia utente, come il look and feel
dell'interfaccia utente, la disposizione geometrica delle schermate, la sequenza delle
interazioni e l'hardware. Per esempio, nel caso di un computer indossabile, un test di
usabilità testerebbe la capacità dell'utente di impartire comandi al sistema mentre è
sdraiato in una posizione scomoda, come nel caso di un meccanico che guarda uno
schermo sotto una macchina mentre controlla una marmitta.
La tecnica per condurre test di usabilità è basata sull'approccio classico per condurre un
esperimento controllato. Gli sviluppatori prima formulano un insieme di obiettivi di test,
descrivendo ciò che sperano di imparare nel test. Questi possono includere, per esempio,
la valutazione di specifiche dimensioni o del layout geometrico dell'interfaccia utente, la
valutazione dell'impatto del tempo di risposta sull'efficienza dell'utente, o la valutazione se
la documentazione della guida in linea è sufficiente per gli utenti inesperti. Gli obiettivi del
test sono poi valutati in una serie di esperimenti in cui i partecipanti sono addestrati a
svolgere compiti predefiniti (ad esempio, esercitando la caratteristica dell'interfaccia
utente in esame). Gli sviluppatori osservano i partecipanti e raccolgono dati che misurano
le prestazioni dell'utente (ad esempio, il tempo per compiere un compito, il tasso di errore)
e le preferenze (ad esempio, opinioni e processi di pensiero) per identificare problemi
specifici con il sistema o raccogliere idee per migliorarlo [Rubin, 1994].
Ci sono due importanti differenze tra gli esperimenti controllati e i test di usabilità. Mentre
il metodo sperimentale classico è progettato per confutare un'ipotesi, l'obiettivo dei test di
usabilità è quello di ottenere informazioni qualitative su come risolvere i problemi di
usabilità e come migliorare il sistema. L'altra differenza è il rigore con cui vengono eseguiti
gli esperimenti. È stato dimostrato che anche una serie di test rapidi e mirati che iniziano
già nella fase di elicitazione dei requisiti è estremamente utile. Nielsen usa il termine
"discount usability engineering" per riferirsi a test di usabilità semplificati che possono
essere eseguiti in una frazione del tempo e del costo di uno studio completo, notando che
pochi test di usabilità sono meglio di nessuno [Nielsen & Mack, 1994]. Esempi di test di
usabilità scontati includono l'uso di mock-up di scenari cartacei (invece di uno scenario
videoregistrato), basandosi su note scritte a mano invece di analizzare le trascrizioni dei
nastri audio, o usando meno soggetti per suscitare suggerimenti e scoprire i difetti
principali (invece di raggiungere la significatività statistica e usare misure quantitative).

Ci sono tre tipi di test di usabilità:

• Test di scenario. Durante questo test, ad uno o più utenti viene presentato uno
scenario visionario del sistema. Gli sviluppatori identificano quanto velocemente gli utenti
sono in grado di capire lo scenario, quanto accuratamente rappresenta il loro modello di
lavoro, e quanto positivamente reagiscono alla descrizione del nuovo sistema. Gli scenari
selezionati dovrebbero essere il più realistici e dettagliati possibile. Un test di scenario
permette un feedback rapido e frequente da parte dell'utente. I test di scenario possono
essere realizzati come mock-up di carta 3o con un semplice ambiente di prototipazione,
che è spesso più facile da imparare rispetto all'ambiente di programmazione utilizzato per
lo sviluppo. Il vantaggio dei test di scenario è che sono economici da realizzare e da
ripetere. Gli svantaggi sono che l'utente non può interagire direttamente con il sistema e
che i dati sono fissi.
• Test del prototipo. Durante questo tipo di test, agli utenti finali viene presentato un
pezzo di software che implementa aspetti chiave del sistema. Un prototipo verticale
implementa completamente un caso d'uso attraverso il sistema. I prototipi verticali sono
usati per valutare i requisiti fondamentali, per esempio il tempo di risposta del sistema o il
comportamento dell'utente sotto stress. Un prototipo orizzontale implementa un singolo
strato nel sistema; un esempio è un prototipo di interfaccia utente, che presenta
un'interfaccia per la maggior parte dei casi d'uso (senza fornire molte o nessuna
funzionalità). I prototipi di interfaccia utente sono usati per valutare questioni come
concetti alternativi di interfaccia utente o layout di finestre. Un prototipo Wizard of Oz è
un prototipo di interfaccia utente in cui un operatore umano dietro le quinte tira le leve
[Kelly, 1984]. I prototipi Wizard of Oz sono usati per testare applicazioni in linguaggio
naturale, quando il riconoscimento vocale o i sottosistemi di analisi del linguaggio naturale
sono incompleti. Un operatore umano intercetta le richieste dell'utente e le riformula in
termini che il sistema comprende, senza che l'utente del test sia consapevole
dell'operatore. I vantaggi dei test dei prototipi sono che forniscono una visione realistica
del sistema all'utente e che i prototipi possono essere strumentati per raccogliere dati
dettagliati. Tuttavia, i prototipi richiedono uno sforzo maggiore per costruire rispetto agli
scenari di test.
• Test del prodotto. Questo test è simile al test del prototipo, tranne che viene usata
una versione funzionale del sistema al posto del prototipo. Un test di prodotto può essere
condotto solo dopo che la maggior parte del sistema è stata sviluppata. Richiede anche che
il sistema sia facilmente modificabile in modo che i risultati del test di usabilità possano
essere presi in considerazione.

In tutti e tre i tipi di test, gli elementi di base dei test di usabilità includono [Rubin, 1994]
• sviluppo degli obiettivi dei test

3. L'uso degli storyboard, una tecnica dell'industria dell'animazione, consiste nello


schizzare una sequenza di immagini dello schermo in diversi punti dello scenario. Le
immagini di ogni scenario sono poi allineate cronologicamente contro una parete su una
tavola (da qui il termine "storyboard"). Gli sviluppatori e gli utenti camminano intorno alla
stanza durante la revisione e la discussione degli scenari. Data una stanza di dimensioni
ragionevoli, i partecipanti possono trattare diverse centinaia di schizzi.

• un campione rappresentativo di utenti finali


• l'ambiente di lavoro reale o simulato
• interrogazione controllata ed estesa e sondaggio degli utenti da parte della persona
che esegue il test di usabilità
• raccolta e analisi dei risultati quantitativi e qualitativi
• raccomandazioni su come migliorare il sistema.

Gli obiettivi tipici di un test di usabilità riguardano il confronto di due stili di interazione con
l'utente, l'identificazione delle caratteristiche migliori e peggiori in uno scenario o in un
prototipo, i principali ostacoli, l'identificazione delle caratteristiche utili per gli utenti
principianti ed esperti, quando è necessario un aiuto, e che tipo di informazioni di
formazione sono necessarie.

11.4.3 Test dell'unità


Il test unitario si concentra sui blocchi di costruzione del sistema software, cioè oggetti e
sottosistemi. Ci sono tre motivazioni per concentrarsi su questi blocchi di costruzione.
Primo, i test unitari riducono la complessità delle attività di test complessive,
permettendoci di concentrarci su unità più piccole del sistema. Secondo, i test unitari
rendono più facile individuare e correggere gli errori, dato che pochi componenti sono
coinvolti nel test. Terzo, i test unitari permettono il parallelismo nelle attività di test; cioè,
ogni componente può essere testato indipendentemente dagli altri.
I candidati specifici per i test unitari sono scelti dal modello degli oggetti e dalla
decomposizione del sistema. In linea di principio, tutti gli oggetti sviluppati durante il
processo di sviluppo dovrebbero essere testati, il che spesso non è fattibile a causa dei
vincoli di tempo e budget. L'insieme minimo di oggetti da testare dovrebbe essere gli
oggetti partecipanti ai casi d'uso. I sottosistemi dovrebbero essere testati come
componenti solo dopo che ogni classe all'interno di quel sottosistema è stata testata
individualmente.
I sottosistemi esistenti, che sono stati riutilizzati o acquistati, dovrebbero essere trattati
come componenti con struttura interna sconosciuta. Questo si applica in particolare ai
sottosistemi disponibili in commercio, dove la struttura interna non è nota o disponibile
allo sviluppatore.
Sono state ideate molte tecniche di test delle unità. Di seguito, descriviamo le più
importanti: test di equivalenza, test di confine, test di percorso e test basato sugli stati.

Test di equivalenza
Questa tecnica di test blackbox minimizza il numero di casi di test. I possibili input sono
partizionati in classi di equivalenza, e un caso di test è selezionato per ogni classe. Il
presupposto del test di equivalenza è che i sistemi di solito si comportano in modo simile
per tutti i membri di una classe. Per testare il comportamento associato ad una classe di
equivalenza, abbiamo solo bisogno di testare un membro della classe. Il test di equivalenza
consiste in due fasi: identificazione delle classi di equivalenza e selezione degli input di test.
I seguenti criteri sono utilizzati per determinare le classi di equivalenza.
• Copertura. Ogni possibile input appartiene a una delle classi di equivalenza.
• Disgiunzione. Nessun input appartiene a più di una classe di equivalenza.
• Rappresentazione. Se l'esecuzione dimostra uno stato errato quando un particolare
membro di una classe di equivalenza viene usato come input, allora lo stesso stato
errato può essere rilevato usando qualsiasi altro membro della classe come input.
Per ogni classe di equivalenza, vengono selezionati almeno due dati: un input tipico, che
esercita il caso comune, e un input non valido, che esercita le capacità di gestione delle
eccezioni del componente. Dopo che tutte le classi di equivalenza sono state identificate, si
deve identificare un input di prova per ogni classe che copra la classe di equivalenza. Se c'è
la possibilità che non tutti gli elementi della classe di equivalenza siano coperti dall'input di
test, la classe di equivalenza deve essere divisa in classi di equivalenza più piccole, e gli
input di test devono essere identificati per ciascuna delle nuove classi.
Per esempio, considerate un metodo che restituisce il numero di giorni in un mese, dati il
mese e l'anno (vedi Figura 11-10). Il mese e l'anno sono specificati come interi. Per
convenzione, 1 rappresenta il mese di gennaio, 2 il mese di febbraio e così via. L'intervallo
di input validi per l'anno è da 0 a maxInt.
Troviamo tre classi di equivalenza per il parametro del mese: mesi con 31 giorni (cioè 1, 3,
5, 7, 8, 10, 12), mesi con 30 giorni (cioè 4, 6, 9, 11), e febbraio, che può avere 28 o 29
giorni. I numeri interi non positivi e i numeri interi maggiori di 12 non sono valori validi per
il parametro del mese. Allo stesso modo, troviamo due classi di equivalenza per l'anno:
anni bisestili e anni non bisestili. Per specificazione, gli interi negativi sono valori non validi
per l'anno. Per prima cosa selezioniamo un valore valido per ogni classe di equivalenza (ad
esempio, febbraio, giugno, luglio, 1901 e 1904). Dato che il valore di ritorno del metodo
getNumDaysInMonth() dipende da entrambi i parametri, combiniamo questi valori per
verificare l'interazione, ottenendo le sei classi di equivalenza mostrate nella Tabella 11-2.

Test di confine

Questo caso speciale di test di equivalenza si concentra sulle condizioni al confine delle
classi di equivalenza. Piuttosto che selezionare qualsiasi elemento nella classe di
equivalenza, il test di confine richiede che gli elementi siano selezionati dai "bordi" della
classe di equivalenza. L'ipotesi

dietro i test di confine è che gli sviluppatori spesso trascurano i casi speciali al confine delle
classi di equivalenza (ad esempio, 0, stringhe vuote, anno 2000).
Nel nostro esempio, il mese di febbraio presenta diversi casi limite. In generale, gli anni che
sono multipli di 4 sono bisestili. Gli anni che sono multipli di 100, tuttavia, non sono
bisestili, a meno che non siano anche multipli di 400. Per esempio, il 2000 è stato un anno
bisestile, mentre il 1900 no. Entrambi gli anni 1900 e 2000 sono buoni casi limite che
dovremmo testare. Altri casi limite includono i mesi 0 e 13, che sono ai confini della classe
di equivalenza non valida. La Tabella 11-3 mostra gli ulteriori casi limite che abbiamo
selezionato per il metodo getNumDaysInMonth().
Uno svantaggio dei test di equivalenza e di confine è che queste tecniche non esplorano le
combinazioni dei dati di input del test. In molti casi, un programma fallisce perché una
combinazione di certi valori causa l'errore. Il test causa-effetto affronta questo problema
stabilendo relazioni logiche tra input e output o input e trasformazioni. Gli ingressi sono
chiamati cause, le uscite o trasformazioni sono effetti. La tecnica si basa sulla premessa che
il comportamento di input/output può essere trasformato in una funzione booleana. Per i
dettagli su questa tecnica e un'altra tecnica chiamata "error guessing", vi rimandiamo alla
letteratura sui test (per esempio [Myers, 1979]).

Test del percorso


Questa tecnica di test whitebox identifica i difetti nell'implementazione del componente. Il
presupposto dietro il path testing è che, esercitando tutti i possibili percorsi attraverso il
codice almeno una volta, la maggior parte dei guasti si innescheranno. L'identificazione dei
percorsi richiede la conoscenza del codice sorgente e delle strutture dati. Il punto di
partenza per il path testing è il grafico di flusso. Un grafico di flusso consiste di nodi che
rappresentano blocchi eseguibili e bordi che rappresentano il flusso di controllo. Un grafico
di flusso è costruito dal codice di un componente mappando le dichiarazioni di decisione
(ad esempio, dichiarazioni if, while loops) ai nodi. Le dichiarazioni tra ogni decisione (ad
esempio, blocco then, blocco else) sono mappate su altri nodi. Le associazioni tra ogni nodo
rappresentano le relazioni di precedenza. La Figura 11-11 mostra un esempio di
implementazione difettosa del metodo getNumDaysInMonth(). La Figura 11-12
rappresenta il grafico di flusso equivalente come un UML
diagramma di attività. In questo esempio, modelliamo le decisioni con rami UML, i blocchi
con azioni UML e il flusso di controllo con transizioni UML.
Il test del percorso completo consiste nel progettare casi di test tali che ogni bordo nel
diagramma di attività sia attraversato almeno una volta. Questo viene fatto esaminando la
condizione associata ad ogni punto di diramazione e selezionando un input per il ramo vero
e un altro input per il ramo falso. Per esempio, esaminando il primo punto di diramazione
nella Figura 11-12, selezioniamo due input: anno=0 (tale che anno < 1 è vero) e anno=1901
(tale che anno <
1 è falso). Ripetiamo poi il processo per il secondo ramo e selezioniamo gli input mese=1 e
mese=2. L'input (anno=0, mese=1) produce il percorso {throw1}. L'input (anno=1901,
mese=1) produce un secondo percorso
{n=32 return}, che scopre uno dei difetti nel metodo getNumDaysInMonth(). Ripetendo
questo processo per ogni nodo, generiamo i casi di test descritti nella Tabella 11-4.
Possiamo similmente costruire il diagramma di attività per il metodo isLeapYear() e ricavare
i casi di test per esercitare il singolo punto di diramazione di questo metodo (Figura 11-13).
Notate che il test case (anno = 1901, mese = 2) del metodo getNumDaysInMonth() esercita
già uno dei
i percorsi del metodo isLeapYear(). Costruendo sistematicamente i test per coprire tutti i
percorsi di tutti i metodi, possiamo affrontare la complessità associata a un gran numero di
metodi.
Usando la teoria dei grafi, si può dimostrare che il numero minimo di test necessari per
coprire tutti i bordi è uguale al numero di percorsi indipendenti attraverso il grafico di
flusso [McCabe, 1976]. Questo è definito come la complessità ciclomatica CC del grafo di
flusso, che è
CC = numero di bordi - numero di nodi + 2
dove il numero di nodi è il numero di rami e azioni, e il numero di bordi è il numero di
transizioni nel diagramma di attività. La complessità ciclomatica del metodo
getNumDaysInMonth() è 6, che è anche il numero di casi di test che abbiamo trovato nella
Tabella 11-4. Allo stesso modo, la complessità ciclica del metodo isLeapYear() e il numero
di casi di test derivati è 2.
Confrontando i casi di test che abbiamo derivato dalle classi di equivalenza (Tabella 11-2) e
i casi limite (Tabella 11-3) con i casi di test che abbiamo derivato dal grafico di flusso
(Tabella 11-4 e Figura 11-13), si possono notare diverse differenze. In entrambi i casi,
testiamo il metodo ampiamente per i calcoli che coinvolgono il mese di febbraio. Tuttavia,
poiché l'implementazione di isLeapYear() non prende in considerazione gli anni divisibili per
100, il test del percorso non ha generato alcun caso di test per questa classe di equivalenza.
In generale, il test del percorso e i metodi whitebox possono rilevare solo gli errori
risultanti dall'esercizio di un percorso nel programma, come l'istruzione numDays=32
difettosa. I metodi di test whitebox non possono rilevare omissioni, come la mancata
gestione dell'anno non bisestile 1900. Il test dei percorsi è anche pesantemente basato
sulla struttura di controllo del programma; i difetti associati alla violazione delle invarianti
delle strutture dati, come l'accesso ad un array fuori dai limiti, non sono esplicitamente
affrontati. Tuttavia, nessun metodo di test, a parte il test esaustivo, può garantire la
scoperta di tutti gli errori. Nel nostro esempio, né i test di equivalenza né i test di percorso
hanno scoperto il difetto associato al mese di agosto.
Test basati sullo stato
Questa tecnica di test è stata recentemente sviluppata per sistemi orientati agli oggetti
[Turner & Robson, 1993]. La maggior parte delle tecniche di test si concentrano sulla
selezione di un certo numero di input di test per un dato stato del sistema, esercitando un
componente o un sistema, e confrontando gli output osservati con un oracolo. Il test
basato sullo stato, tuttavia, confronta lo stato risultante del sistema con lo stato atteso. Nel
contesto di una classe, il test basato sullo stato consiste nel derivare casi di test dal
diagramma della macchina a stati UML per la classe. Per ogni stato, viene derivato un set
rappresentativo di stimoli per ogni transizione (simile al test di equivalenza). Gli attributi
della classe sono quindi strumentati e testati dopo che ogni stimolo è stato applicato per
assicurare che la classe abbia raggiunto lo stato specificato.
Per esempio, la Figura 11-14 rappresenta un diagramma di macchina a stati e i suoi test
associati per il 2Bwatch che abbiamo descritto nel Capitolo 2, Modellare con UML. Esso
specifica quali stimoli cambiano l'orologio dallo stato di alto livello MeasureTime allo stato
di alto livello SetTime. Non mostra gli stati di basso livello dell'orologio quando la data e
l'ora cambiano, sia a causa di azioni dell'utente che a causa del tempo che passa. Gli input
di test nella Figura 11-14 sono stati generati in modo che ogni transizione sia attraversata
almeno una volta. Dopo ogni input, il codice di strumentazione controlla se l'orologio è
nello stato previsto e altrimenti riporta un fallimento. Si noti che alcune transizioni (ad
esempio la transizione 3) sono attraversate più volte, poiché è necessario rimettere
l'orologio nello stato SetTime
(ad esempio, per testare le transizioni 4, 5 e 6). Vengono visualizzati solo i primi otto
stimoli. Gli input di prova per lo stato DeadBattery non sono stati generati.
Attualmente, i test basati sullo stato presentano diverse difficoltà. Poiché lo stato di una
classe è incapsulato, i casi di test devono includere sequenze per mettere le classi nello
stato desiderato prima che determinate transizioni possano essere testate. I test basati
sullo stato richiedono anche la strumentazione degli attributi di classe. Sebbene il test
basato sullo stato non faccia attualmente parte dello stato della pratica, promette di
diventare una tecnica di test efficace per i sistemi orientati agli oggetti non appena verrà
fornita un'automazione adeguata.
Test di polimorfismo
Il polimorfismo introduce una nuova sfida nei test perché permette ai messaggi di essere
legati a diversi metodi in base alla classe del target. Anche se questo permette agli
sviluppatori di riutilizzare il codice in un numero maggiore di classi, introduce anche più
casi da testare. Tutti i possibili binding dovrebbero essere identificati e testati [Binder,
2000].
Consideriamo il pattern di progettazione NetworkInterface Strategy che abbiamo
introdotto nel Capitolo 8, Progettazione di oggetti: Riutilizzare le soluzioni di pattern
(vedere Figura 11- 15). Il design pattern Strategy usa il polimorfismo per proteggere il
contesto (cioè la classe NetworkConnection) dalla strategia concreta (cioè le classi
Ethernet, WaveLAN e UMTS). Per esempio, il metodo NetworkConnection.send() chiama il
metodo NetworkInterface.send() per inviare byte attraverso la NetworkInterface corrente,
indipendentemente dalla strategia concreta effettiva. Questo significa che, a tempo di
esecuzione, l'invocazione del metodo NetworkInterface.send() può essere legata a uno dei
tre metodi, Ethernet.send(), WaveLAN.send(), UMTS.send().
Quando si applica la tecnica del path testing a un'operazione che usa il polimorfismo,
dobbiamo considerare tutti i binding dinamici, uno per ogni messaggio che potrebbe essere
inviato. In NetworkConnect.send() nella colonna di sinistra della Figura 11-16, invochiamo
l'operazione NetworkInterface.send(), che può essere legata ai metodi Ethernet.send(),
WaveLAN.send() o UMTS.send(), a seconda della classe dell'oggetto nif. Per affrontare
esplicitamente questa situazione, espandiamo il codice sorgente originale sostituendo ogni
invocazione di NetworkInterface.send() con una dichiarazione if else annidata che verifica
tutte le sottoclassi diNetworkInterface (colonna destra della Figura 11-16). A seconda della
classe, nif viene lanciato nella classe concreta appropriata e viene invocato il metodo
associato.

Si noti che in alcune situazioni, il numero di percorsi può essere ridotto eliminando la
ridondanza. Per questo esempio, adottiamo semplicemente un approccio meccanico.
Una volta che il codice sorgente è espanso, estraiamo il grafico di flusso (Figura 11-17) e
generiamo casi di test che coprono tutti i percorsi. Questo risulta in casi di test che
esercitano il metodo send() di tutte e tre le interfacce di rete concrete.
Quando sono coinvolte molte interfacce e classi astratte, generare il diagramma di flusso
per un metodo di media complessità può risultare in un'esplosione di percorsi. Questo
illustra, da un lato, come il codice orientato agli oggetti che usa il polimorfismo può
risultare in componenti compatti ed estensibili, e dall'altro, come il numero di casi di test
aumenta quando si cerca di raggiungere una copertura accettabile del percorso.
11.4.4 Test di integrazione
Il test delle unità si concentra sui singoli componenti. Lo sviluppatore scopre i difetti
usando test di equivalenza, test dei confini, test del percorso e altri metodi. Una volta che i
difetti in ogni componente sono stati rimossi e i casi di test non rivelano alcun nuovo
difetto, i componenti sono pronti per essere integrati in sottosistemi più grandi. A questo
punto, è ancora probabile che i componenti contengano difetti, dato che i test stub e i
driver usati durante i test unitari sono solo approssimazioni dei componenti che simulano.
Inoltre, i test unitari non rivelano i difetti associati alle interfacce dei componenti che
risultano da assunzioni non valide quando si chiamano queste interfacce.
Il test di integrazione individua i difetti che non sono stati rilevati durante i test unitari
concentrandosi su piccoli gruppi di componenti. Due o più componenti sono integrati e
testati, e quando non vengono rivelati nuovi difetti, vengono aggiunti altri componenti al
gruppo. Se due componenti vengono testati insieme, lo chiamiamo un doppio test. Testare
tre componenti insieme è un test triplo, e un test con quattro componenti è chiamato un
test quadruplo. Questa procedura permette di testare parti sempre più complesse del
sistema mantenendo la posizione di potenziali guasti relativamente piccoli (cioè, il
componente aggiunto più di recente è di solito quello che innesca i guasti scoperti più di
recente).
Sviluppare stub di test e driver per un test di integrazione sistematico richiede tempo. Per
questa ragione, l'Extreme Programming, per esempio, stabilisce che i driver siano scritti
prima che i componenti siano sviluppati [Beck & Andres, 2005]. L'ordine in cui i componenti
sono testati, tuttavia, può influenzare lo sforzo totale richiesto dal test di integrazione. Un
attento ordine dei componenti può ridurre le risorse necessarie per il test di integrazione
complessivo. Nelle prossime sezioni, discutiamo le strategie di test di integrazione
orizzontale, in cui i componenti sono integrati secondo i livelli, e le strategie di test di
integrazione verticale, in cui i componenti sono integrati secondo le funzioni.

Strategie di test di integrazione orizzontale


Sono stati ideati diversi approcci per implementare una strategia di test di integrazione
orizzontale: big bang testing, bottom-up testing, top-down testing e sandwich testing.
Ognuna di queste strategie è stata originariamente concepita assumendo che la
decomposizione del sistema sia gerarchica e che ognuno dei componenti appartenga a
strati gerarchici ordinati rispetto all'associazione "Call". Queste strategie, tuttavia, possono
essere facilmente adattate a decomposizioni di sistema non gerarchiche. La Figura 11-18
mostra una decomposizione gerarchica del sistema che usiamo per discutere queste
strategie.
La strategia di test big bang presuppone che tutti i componenti siano prima testati
individualmente e poi testati insieme come un unico sistema. Il vantaggio è che non sono
necessari ulteriori stub di test o driver. Anche se questa strategia sembra semplice, il big
bang testing è costoso: se un test scopre un guasto, è impossibile distinguere i guasti
nell'interfaccia da quelli all'interno di un componente. Inoltre, è difficile individuare il
componente specifico (o la combinazione di componenti) responsabile del fallimento,
poiché tutti i componenti del sistema sono potenzialmente esercitati. Questo porta a
strategie di integrazione che integrano solo alcuni componenti alla volta. La strategia di
test bottom-up testa prima ogni componente del livello inferiore individualmente, e poi li
integra con i componenti del livello superiore.
Questo viene ripetuto fino a quando tutti i componenti di tutti i livelli sono combinati. I test
driver sono usati per simulare i componenti dei livelli superiori che non sono ancora stati
integrati. Si noti che non ci sono stub di test
necessario durante i test bottom-up.
La strategia di test top-down testa prima i componenti del livello superiore e poi integra i
componenti del livello successivo. Quando tutti i componenti del nuovo livello sono stati
testati insieme, viene selezionato il livello successivo. Di nuovo, i test aggiungono
incrementalmente un componente alla volta. Questo viene ripetuto fino a quando tutti i
livelli sono combinati e coinvolti nel test. Gli stub di test sono usati per simulare i
componenti dei livelli inferiori che non sono ancora stati integrati. Si noti che i test driver
non sono necessari durante i test top-down.
Il vantaggio del test bottom-up è che i difetti di interfaccia possono essere trovati più
facilmente: quando gli sviluppatori sostituiscono un driver di test per un componente di
livello superiore, hanno un chiaro modello di come funziona il componente di livello
inferiore e delle assunzioni incorporate nella sua interfaccia. Se il
componente di livello superiore viola le assunzioni fatte nel componente di livello inferiore,
gli sviluppatori hanno maggiori probabilità di trovarle rapidamente. Lo svantaggio del test
bottom- up è che testa i sottosistemi più importanti, cioè i componenti dell'interfaccia
utente, per ultimi. I difetti trovati nel livello superiore possono spesso portare a
cambiamenti nella decomposizione del sottosistema o nelle interfacce del sottosistema dei
livelli inferiori, invalidando i test precedenti.
Il vantaggio del test top-down è che inizia con i componenti dell'interfaccia utente. Lo
stesso insieme di test, derivato dai requisiti, può essere usato per testare l'insieme sempre
più complesso di sottosistemi. Lo svantaggio del test top-down è che lo sviluppo di stub di
test richiede molto tempo ed è soggetto ad errori. Un gran numero di stub è solitamente
richiesto per testare sistemi non banali, specialmente quando il livello più basso della
decomposizione del sistema implementa molti metodi.
Le figure 11-19 e 11-20 illustrano le possibili combinazioni di sottosistemi che possono
essere usate durante i test di integrazione. Usando una strategia bottom-up, i sottosistemi
E, F, e G sono testati insieme per primi, poi vengono eseguiti il triplo test B-E-F e il doppio
test D-G, e così via. Usando una strategia top-down, il sottosistema A viene testato
unitariamente, poi vengono eseguiti i test doppi A-B, A-C, e A-D, poi viene eseguito il test
quadruplo A-B-C-D, e così via. Entrambe le strategie coprono lo stesso numero di
dipendenze del sottosistema, ma le esercitano in ordine diverso.
La strategia di test a sandwich combina le strategie top-down e bottom-up, cercando di
fare uso del meglio di entrambe. Durante il test a sandwich, il tester deve essere in grado di
riformulare o mappare la decomposizione del sottosistema in tre strati, uno strato di
destinazione ("la carne"), un
uno strato sopra lo strato di destinazione ("la fetta di pane superiore"), e uno strato sotto
lo strato di destinazione ("la fetta di pane inferiore"). Usando lo strato di destinazione
come centro dell'attenzione, i test top-down e bottom-up possono ora essere fatti in
parallelo. Il test di integrazione top-down viene fatto testando il livello superiore in modo
incrementale con i componenti del livello di destinazione, e il test bottom-up viene usato
per testare il livello inferiore in modo incrementale con i componenti del livello di
destinazione. Come risultato, gli stub di test e i driver non hanno bisogno di essere scritti
per i livelli superiore e inferiore, perché usano i componenti reali del livello di destinazione.
Si noti che questo permette anche di testare in anticipo i componenti dell'interfaccia
utente. C'è un problema con il test a sandwich: non testa a fondo i singoli componenti del
livello di destinazione prima dell'integrazione. Per esempio, il test a sandwich mostrato in
Figura 11-21 non testa unitariamente il componente C del livello di destinazione.

La strategia di test a sandwich modificata testa i tre strati individualmente prima di


combinarli in test incrementali l'uno con l'altro. I test dei singoli strati consistono in un
gruppo di tre test:

• un test del livello superiore con stub per il livello di destinazione


• un test del livello di destinazione con driver e stub che sostituiscono i livelli
superiore e inferiore
• un test dello strato inferiore con un driver per lo strato di destinazione.
I test degli strati combinati consistono in due prove:
• Il livello superiore accede al livello di destinazione. Questo test può riutilizzare i test
del livello di destinazione dai test dei singoli livelli, sostituendo i driver con i
componenti del livello superiore.
• Il livello inferiore è accessibile dal livello di destinazione. Questo test può riutilizzare
i test del livello di destinazione dai test dei singoli livelli, sostituendo lo stub con
componenti del livello inferiore.

Il vantaggio del test a sandwich modificato è che molte attività di test possono essere
eseguite in parallelo, come indicato dai diagrammi di attività delle figure 11-21 e 11-22. Lo
svantaggio del test a sandwich modificato è la necessità di test stub e driver aggiuntivi. Nel
complesso, il test sandwich modificato porta a un tempo di test complessivo
significativamente più breve rispetto ai test top-down o bottom-up.

Strategie di test di integrazione verticale


Nella sezione precedente, abbiamo discusso le strategie di test di integrazione orizzontale,
in cui i componenti sono integrati in strati, seguendo la decomposizione del sottosistema.
Poiché anche le responsabilità di sviluppo seguono la decomposizione del sottosistema,
l'integrazione orizzontale è semplice da gestire, poiché i test verificano le interfacce che
sono state negoziate tra i team. Lo svantaggio principale, tuttavia, è che un sistema
operativo che può essere un candidato al rilascio, è disponibile solo molto tardi durante lo
sviluppo.
Le strategie di test di integrazione verticale, al contrario, si concentrano sull'integrazione
iniziale. Per un dato caso d'uso, le parti necessarie di ogni componente, come l'interfaccia
utente, la logica di business, il middleware e lo storage, sono identificati e sviluppati in
parallelo e testati per l'integrazione. Si noti che questo è diverso dai prototipi verticali per i
test di usabilità discussi nella Sezione 11.4.2, poiché i prototipi verticali non sono candidati
al rilascio. Un sistema costruito con una strategia di integrazione verticale produce
candidati al rilascio.
Per esempio, Extreme Programming [Beck & Andres, 2005], usa una strategia di
integrazione verticale in termini di storie utente. Una user story è un singolo requisito
funzionale formulato dal cliente che viene realizzato e testato durante un'iterazione. Alla
fine di un'iterazione, una release candidate viene prodotta e dimostrata al cliente. Lo
svantaggio del test di integrazione verticale, tuttavia, è che il design del sistema si evolve in
modo incrementale, spesso con il risultato di riaprire importanti decisioni di design del
sistema.
Discutiamo le soluzioni alle prime sfide di integrazione quando si introduce l'integrazione
continua nel Capitolo 13, Gestione della configurazione.

11.4.5 Test del sistema


I test di unità e integrazione si concentrano sulla ricerca di difetti nei singoli componenti e
nelle interfacce tra i componenti. Una volta che i componenti sono stati integrati, il test di
sistema assicura che il sistema completo sia conforme ai requisiti funzionali e non
funzionali. Si noti che il test di integrazione verticale è un caso speciale di test di sistema: il
primo si concentra solo su una nuova fetta di funzionalità, mentre il test di sistema si
concentra sul sistema completo.
Durante il test del sistema, vengono eseguite diverse attività:

• Test funzionale. Test dei requisiti funzionali (da RAD)


• Test delle prestazioni. Test dei requisiti non funzionali (da SDD)
• Test pilota. Test di funzionalità comuni tra un gruppo selezionato di utenti finali
nell'ambiente di destinazione.
• Test di accettazione. Test di usabilità, funzionali e di performance eseguiti dal
cliente nell'ambiente di sviluppo contro i criteri di accettazione (dal Project
Agreement)
• Test di installazione. Test di usabilità, funzionali e di performance eseguiti dal
cliente nell'ambiente di destinazione. Se il sistema viene installato solo presso un
piccolo gruppo selezionato di clienti, si chiama beta test.

Test funzionali
Il test funzionale, chiamato anche test dei requisiti, trova le differenze tra i requisiti
funzionali e il sistema. Il test funzionale è una tecnica blackbox: i casi di test sono derivati
dal modello dei casi d'uso. Nei sistemi con requisiti funzionali complessi, di solito non è
possibile testare tutti i casi d'uso per tutti gli input validi e non validi. L'obiettivo del tester è
di selezionare quei test che sono rilevanti per l'utente e hanno un'alta probabilità di
scoprire un fallimento. Si noti che il test funzionale è diverso dal test di usabilità (descritto
nel Capitolo 4, Elicitazione dei requisiti), che si concentra anche sul modello dei casi d'uso.
Il test funzionale trova le differenze tra il modello dei casi d'uso e il comportamento del
sistema osservato, mentre il test di usabilità trova le differenze tra il modello dei casi d'uso
e l'aspettativa dell'utente del sistema.
Per identificare i test funzionali, ispezioniamo il modello del caso d'uso e identifichiamo le
istanze del caso d'uso che probabilmente causeranno dei fallimenti. Questo viene fatto
usando tecniche di blackbox simili ai test di equivalenza e ai test di confine (vedi Sezione
11.4.3). I casi di test dovrebbero esercitare sia i casi d'uso comuni che quelli eccezionali. Per
esempio, si consideri il modello di caso d'uso per un distributore di biglietti della
metropolitana (vedi Figura 11-23). La funzionalità del caso comune è modellata dal caso
d'uso PurchaseTicket, che descrive i passi necessari a un passeggero per acquistare con
successo un biglietto. I casi d'uso TimeOut, Cancel, OutOfOrder e NoChange descrivono
varie condizioni eccezionali derivanti dallo stato del distributore o dalle azioni del
passeggero.

La figura 11-24 mostra il caso d'uso PurchaseTicket che descrive la normale interazione tra
l'attore Passenger e il Distributor. Notiamo che tre caratteristiche del Distributore

1. Il passeggero può premere più pulsanti di zona prima di inserire il denaro, nel qual
caso il distributore dovrebbe visualizzare l'importo dell'ultima zona.
2. Il passeggero può selezionare un altro pulsante di zona dopo aver iniziato a inserire
denaro, nel qual caso il distributore deve restituire tutto il denaro inserito dal
passeggero.
3. Il Passeggero può inserire più denaro del necessario, nel qual caso il Distributore
dovrebbe restituire il cambiamento corretto.
La figura 11-25 mostra il caso di test PurchaseTicket_CommonCase, che esercita
queste tre caratteristiche. Si noti che il flusso di eventi descrive sia gli input al
sistema (stimoli che il passeggero invia al distributore) che gli output desiderati
(risposte corrette dal distributore). Casi di test simili possono essere derivati anche
per i casi d'uso eccezionali NoChange, OutOfOrder, TimeOut e Cancel.
I casi di test, come PurchaseTicket_CommonCase, sono derivati per tutti i casi d'uso,
compresi i casi d'uso che rappresentano comportamenti eccezionali. I casi di test
sono associati ai casi d'uso da cui sono derivati, rendendo più facile l'aggiornamento
dei casi di test quando i casi d'uso vengono modificati.

Test delle prestazioni


Il test delle prestazioni trova le differenze tra gli obiettivi di progettazione
selezionati durante la progettazione del sistema e il sistema. Poiché gli obiettivi di
progettazione sono derivati dai requisiti non funzionali, i casi di test possono essere
derivati dall'SDD o dal RAD. I seguenti test vengono eseguiti durante il test delle
prestazioni:
• Lo stress test verifica se il sistema può rispondere a molte richieste
simultanee. Per esempio, se un sistema informativo per concessionari d'auto
deve interfacciarsi con 6000 concessionari, lo stress test valuta come il
sistema si comporta con più di 6000 utenti simultanei.
• Il test del volume cerca di trovare i difetti associati a grandi quantità di dati,
come i limiti statici imposti dalla struttura dei dati, o algoritmi ad alta
complessità, o un'elevata frammentazione del disco.
• I test di sicurezza cercano di trovare i difetti di sicurezza nel sistema. Ci sono
pochi metodi sistematici per trovare i difetti di sicurezza. Di solito questo
test è realizzato da "squadre di tigre" che tentano di penetrare nel sistema,
usando la loro esperienza e conoscenza dei tipici difetti di sicurezza.
• Il test dei tempi cerca di trovare i comportamenti che violano i vincoli di
tempo descritti dai requisiti non funzionali.
• I test di recupero valutano la capacità del sistema di recuperare da stati
errati, come l'indisponibilità di risorse, un guasto hardware o un guasto di
rete.

Dopo che tutti i test funzionali e di performance sono stati eseguiti, e nessun guasto
è stato rilevato durante questi test, si dice che il sistema è stato convalidato.
Test pilota
Durante il test pilota, chiamato anche test sul campo, il sistema viene installato e
usato da un gruppo selezionato di utenti. Gli utenti esercitano il sistema come se
fosse stato installato in modo permanente. Agli utenti non vengono date linee guida
esplicite o scenari di test. I test pilota sono utili quando un sistema viene costruito
senza un insieme specifico di requisiti o senza un cliente specifico in mente. In
questo caso, un gruppo di persone è invitato ad usare il sistema per un tempo
limitato e a dare il proprio feedback agli sviluppatori.
Un test alfa è un test pilota con gli utenti che esercitano il sistema nell'ambiente di
sviluppo. In un beta test, il test pilota è eseguito da un numero limitato di utenti
finali nell'ambiente di destinazione; cioè, la differenza tra i test di usabilità e i test
alfa o beta è che il comportamento dell'utente finale non è osservato e registrato.
Di conseguenza, i test beta non testano i requisiti di usabilità in modo così
approfondito come fanno i test di usabilità. Per i sistemi interattivi in cui la facilità
d'uso è un requisito, il test di usabilità non può quindi essere sostituito da un beta
test.
Internet ha reso la distribuzione del software molto facile. Di conseguenza, i beta
test sono sempre più comuni. Infatti, alcune aziende ora lo usano come metodo
principale per testare il loro software. Poiché il processo di download è
responsabilità dell'utente finale, non degli sviluppatori, il costo di distribuzione del
software sperimentale è diminuito notevolmente. Di conseguenza, anche un
numero limitato di beta tester è una questione del passato. Il nuovo paradigma del
beta test offre il software a chiunque sia interessato a testarlo. Infatti, alcune
aziende fanno pagare i loro utenti per testare il loro software in versione beta!

Test di accettazione
Ci sono tre modi in cui il cliente valuta un sistema durante il test di accettazione. In
un test di benchmark, il cliente prepara un insieme di casi di test che rappresentano
condizioni tipiche in cui il sistema dovrebbe funzionare. I test di benchmark possono
essere eseguiti con utenti reali o da un team di test speciale che esercita le funzioni
del sistema, ma è importante che i tester abbiano familiarità con i requisiti
funzionali e non funzionali in modo da poter valutare il sistema.
Un altro tipo di test di accettazione del sistema è usato nei progetti di
reingegnerizzazione, quando il nuovo sistema sostituisce un sistema esistente. Nel
test del concorrente, il nuovo sistema viene testato contro un sistema esistente o
un prodotto concorrente. Nel test ombra, una forma di test di confronto, il nuovo
sistema e il sistema legacy vengono eseguiti in parallelo e le loro uscite vengono
confrontate.
Dopo il test di accettazione, il cliente riferisce al project manager quali requisiti non
sono soddisfatti. Il test di accettazione dà anche l'opportunità di un dialogo tra gli
sviluppatori e il cliente sulle condizioni che sono cambiate e quali requisiti devono
essere aggiunti, modificati o cancellati a causa dei cambiamenti. Se i requisiti
devono essere cambiati, i cambiamenti dovrebbero essere riportati nel verbale della
revisione di accettazione del cliente e dovrebbero costituire la base per un'altra
iterazione del processo del ciclo di vita del software. Se il cliente è soddisfatto, il
sistema viene accettato, possibilmente in funzione di un elenco di modifiche
registrate nel verbale del test di accettazione.

Test di installazione
Dopo che il sistema è stato accettato, viene installato nell'ambiente di destinazione.
Un buon piano di test del sistema permette la facile riconfigurazione del sistema
dall'ambiente di sviluppo all'ambiente di destinazione. Il risultato desiderato del
test di installazione è che il sistema installato soddisfi correttamente tutti i requisiti.
Nella maggior parte dei casi, il test di installazione ripete i casi di test eseguiti
durante i test di funzionalità e prestazioni nell'ambiente di destinazione. Alcuni
requisiti non possono essere eseguiti nell'ambiente di sviluppo perché richiedono
risorse specifiche per il target. Per testare questi requisiti, ulteriori casi di test
devono essere progettati ed eseguiti come parte del test di installazione. Una volta
che il cliente è soddisfatto dei risultati del test di installazione, il test del sistema è
completo, e il sistema è formalmente consegnato e pronto per il funzionamento.

Potrebbero piacerti anche