Concetto: Test-first Design
Questa linea guida discute la progettazione Test-first Design. Test-first Design viene applicato creando innanzitutto uno script di test, prima di scrivere e testare il codice. Questa tecnica viene continuata finché non è presente più alcun elemento da testare.
Relazioni
Elementi correlati
Descrizione principale

Introduzione

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.

Diagramma dell'ubicazione di progettazione del test nel ciclo di vita

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:

Diagramma di Test-first Design

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?

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Esempi

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:

  1. 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.

  2. 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.

  3. 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.

Diagramma di stato HVAC

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.

Diagramma di correlazione classi Creditore e Fattura

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.

Chi effettua la test-first design?

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.

E' possibile eseguire l'intera progettazione del test al momento della progettazione del componente?

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().

Istanza del diagramma di sequenza di Security Manager

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.

Test-first design e fasi del RUP

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.