Le progettazioni dei test vengono create utilizzando le informazioni provenienti da vari prodotti di lavoro, compresi i
prodotti di lavoro della progettazione ad esempio le realizzazioni dei casi d'uso, i modelli di progettazione o le
interfacce del classificatore. I test vengono eseguiti dopo la creazione dei componenti. E' una consuetudine creare le
progettazioni dei test prima che di eseguire i test, dopo la creazione dei prodotti di lavoro della progettazione
software. Nella figura 1, riportata di seguito, viene indicato un esempio. In questo caso, la progettazione del test
inizia verso la fine dell'implementazione. Essa viene fornita in base ai risultati della progettazione del componente.
La freccia da Implementazione a Esecuzione del test indica che non è possibile eseguire i test finché l'implementazione
non è completa.
Fig1: tradizionalmente, la progettazione di un test viene eseguita in un secondo momento nel ciclo di vita
Tuttavia, non deve essere necessariamente così. Sebbene occorra attendere l'implementazione del componente prima di
eseguire il test, la progettazione del test può essere eseguita prima. Essa può avvenire subito dopo il completamento
del prodotto di lavoro della progettazione. Oppure può essere effettuata in parallelo con la progettazione del
componente, come mostrato nell'esempio:
Fig2: Test-first Design porta la progettazione del test in linea con la progettazione software
Il processo di spostamento dell'impegno del test "controcorrente" è indicato come "test-first design". Quali sono i
vantaggi?
-
Non importa quanto sia attenta la progettazione del software, verranno commessi degli errori. Ci si potrebbe
dimenticare un fattore importante. O si potrebbero avere determinate abitudini di pensiero che rendono difficile
concepire determinate alternative. O la stanchezza potrebbe avere il sopravvento e far trascurare qualche elemento.
L'analisi del lavoro di progettazione da parte di altre persone aiuta. Tali persone potrebbero notare gli elementi
mancanti o che sono stati trascurati. E' meglio se queste persone hanno una prospettiva differente dalla propria;
esaminando la progettazione da un altro punto di vista, potranno sicuramente notare i fattori che sono stati
dimenticati.
L'esperienza ha mostrato che la prospettiva di verifica è molto efficace. E' estremamente concreta. Durante la
progettazione software, è semplice pensare a un determinato campo che "visualizza il titolo del cliente corrente" e
andare oltre senza pensarci sul serio. Durante la progettazione del test, occorre decidere in modo specifico
quale campo verrà visualizzato quando un cliente che è andato in pensione dalla marina ed ha ottenuto una laurea in
giurisprudenza insiste nel fare riferimento a se stesso come "Egr. Tenente Morton H. Throckbottle (pens.), Egr." Il
suo titolo è "Tenente" o "Egregio"?
Se la progettazione del test viene posticipata esattamente prima dell'esecuzione del test, come avviene nella
Figura 1, probabilmente si avrà uno spreco di denaro. Un errore nella progettazione del software passerà
inosservato finchè un qualsiasi tester dirà "Sai, conosco questo tale della marina...", creando il test "Morton" e
scovando il problema. A questo punto occorre riscrivere un'implementazione parziale o completa e aggiornare il
prodotto di lavoro della progettazione. Sarebbe più economico rilevare il problema prima che inizi
l'implementazione.
-
Alcuni errori possono essere colti prima della progettazione del test. Essi verranno rilevati dall'implementatore.
E' comunque una soluzione poco valida. L'implementazione rallenterà fino ad arrestarsi mentre il punto focale passa
dalla modalità di implementazione della progettazione alle operazioni che la progettazione dovrebbe eseguire. Ciò
risulta disgregativo anche quando i ruoli di implementatore e di progettista appartengono alla stessa persona; è
ancor più deleterio se appartengono a persone differenti. La prevenzione di tale problema è un altro modo in cui
test-first design è di supporto per migliorare l'efficienza.
-
Le progettazioni dei test aiutano gli implementatori in un altro modo: rendendo più chiara la progettazione. Se
l'implementatore si pone delle domande sul significato della progettazione, la progettazione del test può servire
come esempio specifico della funzionalità desiderata. Ciò eviterà molti dei bug dovuti ai malintesi
dell'implementatore.
-
Si verificheranno meno bug anche se la domanda non era presente nella mente dell'implementatore, ma avrebbe
dovuto esserlo. Ad esempio, potrebbe essersi verificata un'ambiguità tra ciò che il progettista ha inconsciamente
interpretato in un modo e l'implementatore in un altro. Se l'implementatore sta lavorando sia in base alla
progettazione che a istruzioni specifiche relative alle funzioni che il componente dovrebbe eseguire, in base agli
scenari di test, è più probabile che il componente esegua effettivamente le operazioni indicate.
Di seguito vengono riportati degli esempi per chiarire il senso di test-first design.
Si supponga che sta creando un sistema per sostituire il vecchio metodo di "richiesta alla segretaria" per
l'assegnazione delle sale delle riunioni. Uno dei metodi della classe MeetingDatabase viene indicato come
getMeeting e presenta questa firma:
Riunione getMeeting(Persona, Ora);
Specificati una persona e un'ora, getMeeting restituisce la riunione a cui quella data persona deve partecipare
in una data specifica. Se non esiste alcuna pianificazione per la persona, l'oggetto speciale Meeting viene
restituito come non pianificato. Di seguito vengono riportati degli scenari di test molto semplici:
-
La persona non partecipa ad alcuna riunione nella data specificata. Viene restituita una riunione non
pianificata?
-
La persona partecipa a una riunione nella data specificata. Il metodo restituisce la riunione corretta?
Questi scenari di test sono poco entusiasmanti, ma alla fine devono essere sperimentati. Potrebbero anche essere creati
in questo momento, scrivendo il codice del test reale che verrà eseguito in un secondo tempo. Il codice Java per il
primo test potrebbe essere simile al seguente:
// se non è presente in una riunione nella data specificata, // è previsto come non pianificato. public void testWhenAvailable() { Person fred = new Person("fred"); Time now = Time.now(); MeetingDatabase db = new MeetingDatabase(); expect(db.getMeeting(fred, now) == Meeting.unscheduled); }
Ma esistono proposte di test più interessanti. Ad esempio, tale metodo ricerca una corrispondenza. Ogni qualvolta il
metodo effettua una ricerca, è una buona idea domandare cosa succederebbe se la ricerca individuasse più
corrispondenze. In questo caso, significa chiedersi "E' possibile che una persona sia presente in due riunioni
contemporaneamente? Sembra impossibile ma, se si rivolgesse la domanda alla segretaria, la risposta potrebbe essere
sorprendente. Verrebbe fuori che alcuni dirigenti vengono spesso pianificati in due riunioni contemporaneamente. Il
loro ruolo è comparire in una riunione, "radunare le truppe" per un periodo di tempo limitato e andare via. Un sistema
che non prevede tale funzionalità non verrà utilizzato così frequentemente.
Questo è un esempio di test-first design effettuato a livello di implementazione per cogliere un problema di analisi.
Occorre tenere presenti alcune considerazioni:
-
Si potrebbe sperare che una analisi e una definizione approfondite del caso d'uso avrebbero potuto già rilevare
questo requisito. In tal caso, il problema sarebbe stato evitato "controcorrente" e getMeeting sarebbe stato
progettato in maniera differente. (Non avrebbe restituito una riunione ma un insieme di riunioni). Ma durante
l'analisi, spesso vengono trascurati alcuni problemi ed è meglio individuarli durante l'implementazione anziché
dopo la distribuzione.
-
In molti casi, progettisti e implementatori non disporranno delle conoscenze di dominio per cogliere tali problemi,
né avranno l'occasione o il tempo per chiedere alla segretaria. In tal caso, il progettista del test per
getMeeting dovrà chiedersi "è possibile che si verifichi un caso in cui devono essere restituite due
riunioni?" , quindi ci penserà per qualche istante e concluderà che non è possibile. Quindi test-first design non
coglie ogni problema, ma il semplice fatto di porsi le domande giuste aumenta la possibilità di rilevarlo.
-
Alcune delle tecniche di verifica che si applicano durante l'implementazione si applicano anche all'analisi.
Test-first design può essere effettuata anche dagli analisti, ma ciò esula dagli argomenti trattati in questa
sezione.
Il secondo dei tre esempi è un modello di diagramma di stato per un sistema di riscaldamento.
Fig 3: Diagramma di stato HVAC
Una serie di test attraverserebbe tutti gli archi del diagramma di stato. Un test potrebbe iniziare con un sistema
inattivo, inserire un evento eccessivamente caldo, interrompere il sistema durante lo stato di
raffreddamento/esecuzione, eliminare l'errore, inserire un altro evento eccessivamente caldo, quindi rieseguire il
sistema e riportarlo nello stato Inattivo. Dal momento che ciò non utilizza tutti gli archi, sono necessari ulteriori
test. Questi tipi di test ricercano diversi tipi di problemi di implementazione. Ad esempio, attraversando ogni arco,
controllano se l'implementazione ne ha lasciato fuori qualcuno. Utilizzando le sequenze di eventi che presentano
percorsi di errore seguiti da percorsi che dovrebbero essere completati correttamente, controllano se il codice di
gestione errori non è riuscito a eliminare i risultati parziali che potrebbero interessare un successivo calcolo. (Per
ulteriori informazioni sul test di diagrammi di stato consultare Guida del prodotto di lavoro: proposte di test per diagrammi di attività e di stato.)
L'esempio finale utilizza parte di un modello di progettazione. Esiste una correlazione tra un creditore e una fattura,
dove qualsiasi creditore specificato può avere più fatture in sospeso.
Fig4: Correlazione tra classi Creditore e Fattura
I test basati su questo modello vengono effettuati sul sistema quando un creditore non ha fatture, quando ne ha una e
quando ne ha diverse. Un tester dovrebbe chiedersi anche se esistono casi in cui occorre associare una fattura a più
creditori o quando una fattura non ha alcun creditore. (Probabilmente le persone che attualmente eseguono il sistema
cartaceo che il sistema di computer dovrà sostituire utilizzano le fatture senza creditore come un modo per tenere
traccia del lavoro in sospeso). In tal caso, questo potrebbe essere un altro problema che l'analisi avrebbe dovuto
rilevare.
Test-first design può essere effettuata dall'autore della progettazione o da un'altra persona. E' frequente che sia
l'autore ad eseguirla. Il vantaggio è la riduzione della comunicazione in eccesso. Il progettista del prodotto di
lavoro e il progettista del test non dovranno spiegarsi reciprocamente le cose. Inoltre, un progettista di test
separato impiegherebbe del tempo a imparare la progettazione, mentre il progettista originale già la conosce. Infine,
molte delle domande del tipo "cosa succede se il compressore entra nello stato X?" sono domande naturali da chiedere
durante la progettazione del test e del prodotto di lavoro software, quindi è preferibile che sia la stessa persona a
porle esattamente una volta e a trascrivere le risposte sotto forma di test.
Tuttavia, esistono degli svantaggi. Il primo è che il progettista del prodotto di lavoro è, in qualche modo, cieco nei
confronti dei propri errori. Il processo di progettazione del test rivelerà una parte di tale "cecità", ma
probabilmente non quanto potrebbe cogliere una persona differente. Quanto questo possa costituire un problema, in
realtà, varia da persona a persona ed è spesso correlato all'esperienza individuale del progettista.
Un altro svantaggio dovuto al fatto che la stessa persona effettua la progettazione software e la progettazione del
test è che non esiste alcun parallelismo. Sebbene per l'assegnazione dei ruoli a persone distinte occorre un ulteriore
impegno totale, il risultato sarà probabilmente un minore dispendio del tempo del calendario. Se le persone non vedono
l'ora di terminare la progettazione e di iniziare l'implementazione, l'impiego di tempo per la progettazione del test
potrebbe essere frustrante. E, fattore da non trascurare, esiste una tendenza a risparmiare sul lavoro per andare
avanti.
No. La ragione è che non tutte le decisioni vengono prese al momento della progettazione. Le decisioni prese durante
l'implementazione non verranno testate correttamente dai test creati dalla progettazione. L'esempio classico di ciò è
una routine di matrici di ordinamento. Esistono molti algoritmi di ordinamento differenti con compromessi differenti.
Quicksort è generalmente più rapido di un ordinamento di inserimento su matrici di grandi dimensioni, ma spesso è più
lento su matrici di dimensioni ridotte. E' quindi possibile implementare un algoritmo di ordinamento per utilizzare
Quicksort per matrici con più di 15 elementi, altrimenti l'ordinamento di inserimento. Tale divisione del lavoro
potrebbe essere invisibile dai prodotti di lavoro della progettazione. E' possibile rappresentarla in un
prodotto di lavoro della progettazione, ma il progettista potrebbe avere deciso che il vantaggio di tale scelta
esplicita non valga la pena. Dal momento che la dimensione della matrice non ha alcun ruolo nella progettazione, la
progettazione del test potrebbe inavvertitamente utilizzare solo matrici di piccole dimensioni, ad indicare che nessuno
dei codici Quicksort verrà testato.
In un altro esempio, considerare questa frazione di un diagramma di sequenza. Esso mostra un SecurityManager che
chiama il metodo log() di StableStore. Tuttavia, in questo caso, log() restituisce un errore, che
fa in modo che SecurityManager chiami Connection.close().
Fig5: Istanza del diagramma di sequenza di SecurityManager
Questo è un ottimo promemoria per l'implementatore. Ogni qualvolta log() incontra un errore, è necessario
interrompere la connessione. La domanda a cui la verifica deve rispondere è se l'implementatore ha realmente effettuato
l'operazione e se l'ha eseguita correttamente in tutti i casi o solo in alcuni. Per rispondere alla
domanda, il progettista del test deve individuare tutte le chiamate a StableStore.log() e verificare che ad
ognuno di tali punti di chiamata venga fornito un errore da gestire.
Potrebbe sembrare strana l'esecuzione di tale test, considerato che è appena stato esaminato l'intero codice che chiama
StableStore.log(). Non è possibile controllare soltanto che esso gestisca correttamente l'errore?
Probabilmente un esame potrebbe essere sufficiente. Ma il codice di gestione degli errori è notoriamente incline agli
errori perché spesso dipende implicitamente dalle presupposizioni che l'esistenza dell'errore sia stata violata.
L'esempio classico di ciò è il codice che gestisce gli errori di assegnazione. Di seguito ne viene riportato un
esempio:
while (true) { // loop di eventi di livello superiore try { XEvent xe = getEvent(); ... // corpo principale del programma } catch (OutOfMemoryError e) { emergencyRestart(); } }
Questo codice tenta il ripristino in seguito a errori di memoria tramite ripulitura (rendendo la memoria disponibile) e
quindi continuando ad elaborare gli eventi. Si supponga che è una progettazione accettabile. emergencyRestart si
impegna a non assegnare memoria. Il problema è che emergencyRestart chiama una qualche routine di programmi di
utilità, che chiama un'altra routine di tale genere, che a sua volta chiama un'altra routine di tale genere che assegna
un nuovo oggetto. Ad eccezione di ciò, non è presente memoria, quindi l'intero programma ha esito negativo. Questi tipi
di problemi sono difficili da rilevare tramite un esame.
Fino a questo punto, si è dato per scontato che la progettazione del test è stata eseguita per quanto possibile e il
più rapidamente possibile. Ossia, sono stati tratti tutti i test possibili dal prodotto di lavoro della progettazione,
aggiungendo, in un secondo momento, solo i test basati sugli elementi interni dell'implementazione. Ciò potrebbe non
essere appropriato nella fase di elaborazione, perché è possibile che la verifica completa non sia allineata con gli
obiettivi di un'iterazione.
Si supponga che è in corso la creazione di un prototipo strutturale per dimostrare agli investitori l'attuabilità di un
prodotto. Questa potrebbe essere basata su alcune istanze chiave del caso d'uso. Sarebbe opportuno testare il codice
per verificare che le supporti. Ma esiste un rischio qualora venissero creati ulteriori test? Ad esempio, potrebbe
essere ovvio che il prototipo ignora importanti casi di errore. Perché non documentare la necessità di tale gestione di
errori scrivendo degli scenari di test che la verifichino?
Ma cosa succede se il prototipo fa il suo lavoro e rivela che l'approccio strutturale non funzionerà? La struttura
verrà eliminata, insieme a tutti i test per la gestione dell'errore. In tal caso, l'impegno di progettazione dei test
non ha portato ad alcun risultato. Sarebbe stato meglio attendere e progettare solo i test necessari per controllare se
tale prototipo con proof-of-concept provasse effettivamente il concetto.
Questo sembra essere un punto di minore importanza, ma sono in gioco effetti psicologici importanti. La fase di
elaborazione sta per portare alla luce i rischi principali. L'intero team del progetto dovrebbe prestare la propria
attenzione su tale rischi. Se le persone si concentrano sulle questioni minori, l'intero gruppo ci rimetterà in
concentrazione ed energia.
Quindi dove è possibile utilizzare correttamente test-first design nella fase di elaborazione? Essa può giocare un
ruolo importante nell'analisi adeguata dei rischi strutturali. La considerazione di quanto precisamente il team sappia
se un rischio è stato realizzato o evitato aggiungerà chiarezza al processo di progettazione e potrà anche dare come
risultato la creazione di una struttura migliore.
Durante la fase di costruzione, i prodotti di lavoro della progettazione vengono espressi nella loro forma finale.
Tutte le realizzazioni del caso d'uso necessarie vengono implementate, così come le interfacce per tutte le classi. Dal
momento che l'obiettivo della fase è la completezza, il completamento di test-first design è appropriato. In seguito
gli eventi dovrebbero invalidare alcuni test, se presenti.
Le fasi di inizio e di transizione generalmente sono meno incentrate sulle attività di progettazione per cui la
verifica è appropriata. In tal caso, test-first design è applicabile. Ad esempio, è possibile utilizzarla con un lavoro
di proof-of-concept candidato nella fase di inizio. Come avviene con la verifica della fase di costruzione e di
elaborazione, dovrebbe essere in linea con gli obiettivi di iterazione.
|