Esplora E-book
Categorie
Esplora Audiolibri
Categorie
Esplora Riviste
Categorie
Esplora Documenti
Categorie
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.
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).
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.
• 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
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.
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]).
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.
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.
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.
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.