Introduzione
L'espressione "Verifica dello sviluppatore" viene utilizzata per suddividere in categorie le attività di verifica
eseguite in modo più appropriato dagli sviluppatori di software. Essa include inoltre i prodotti di lavoro creati da
tali attività. La Verifica dello sviluppatore comprende il lavoro che, tradizionalmente, rientra nelle seguenti
categorie: Verifica dell'unità, buona parte della Verifica dell'integrazione e alcuni aspetti di quanto viene
comunemente indicato come Verifica del sistema. Mentre la Verifica dello sviluppatore è tradizionalmente associata ad
attività nella disciplina Implementazione, essa è inoltre correlata ad attività nella disciplina Analisi e
Progettazione.
Pensando alla Verifica dello sviluppatore in questo modo "olistico", è possibile ridurre una parte dei rischi associati
all'approccio più "atomistico" utilizzato tradizionalmente. Nell'approccio tradizionale alla Verifica dello
sviluppatore, inizialmente l'impegno è incentrato sulla valutazione del fatto che tutte le unità lavorino in modo
indipendente. In seguito nel ciclo di sviluppo, non appena il processo di sviluppo si avvicina al completamento, le
unità integrate vengono assemblate in un sistema o sottosistema di lavoro e verificate in questa impostazione per la
prima volta.
Questo approccio ha numerose pecche. Innanzitutto, dal momento che incoraggia un approccio organizzato alla verifica
delle unità integrate e dei successivi sottosistemi, qualsiasi errore identificato durante tali test viene spesso
individuato troppo tardi. Questa rilevazione in ritardo generalmente comporta la decisione, da parte dell'utente, di
non effettuare alcuna azione di correzione o richiede un ulteriore lavoro di correzione. Questa rilavorazione è
dispendiosa e sottrae del tempo ad eventuali operazioni in altre aree. In questo modo, aumenta il rischio che il
progetto venga sviato o abbandonato.
In secondo luogo, la creazione di confini rigidi tra Unità, Integrazione e Test del sistema aumenta la probabilità che
gli errori che si verificano oltre tali confini non vengano mai rilevati. Il rischio è calcolato quando la
responsabilità per questi tipi di test è assegnata a team separati.
Lo stile della verifica dello sviluppatore consigliato da RUP incoraggia lo sviluppatore a concentrarsi sui test più
validi e appropriati da condurre nell'ubicazione specificata al momento opportuno. Anche all'interno dell'ambito di una
singola iterazione, è generalmente più efficace per lo sviluppatore individuare e correggere il maggior numero
possibile di difetti nel proprio codice, senza l'ulteriore sovraccarico nell'hand-off a un gruppo di test separato. Il
risultato che si auspica è la rilevazione anticipata degli errori software più significativi, indipendentemente dal
fatto che tali errori si trovino nell'unità indipendente, l'integrazione delle unità o la gestione delle unità
integrate all'interno di uno scenario di utente finale significativo.
Problemi durante la prima esecuzione della
Verifica dello sviluppatore
Molti sviluppatori che iniziano con il tentativo di effettuare un lavoro più completo di verifica rinunciano dopo
pochissimo tempo. Il motivo è che ritengono non ne valga la pena. Inoltre, alcuni sviluppatori che iniziano in modo
positivo con la verifica ritengono di avere creato una suite di test non gestibile che, alla fine, viene abbandonata.
Questa pagina fornisce alcune linee guida per superare i primi intoppi e per la creazione di una suite di test che
evita il problema della gestione. Per ulteriori informazioni, consultare Linea guida: Manutenzione delle suite di test automatizzate.
Aspettative
Ci sono utenti che considerano la verifica dello sviluppatore un lavoro che vale la pena svolgere. Ce ne sono altri che
considerano un lavoro ingrato individuare modalità per evitare di eseguirlo. Questa è semplicemente la natura della
maggior parte degli sviluppatori nella maggior parte delle aziende e trattarla come una ignobile mancanza di disciplina
non ha mai riscosso tanto successo. Quindi, lo sviluppatore in quanto tale deve aspettarsi che la verifica sia
remunerativa sia nel processo che nel risultato a cui conduce.
La verifica ideale dello sviluppatore segue un loop di test della modifica molto rigido. Viene apportata una modifica
insignificante al prodotto, come ad esempio l'aggiunta di un nuovo metodo a una classe, quindi vengono rieseguiti
immediatamente i test. Se un test viene interrotto, è subito chiaro quale codice ne sia la causa. Questo ritmo semplice
e stabile dello sviluppo è la ricompensa più grande della verifica dello sviluppatore. L'unica eccezione potrebbe
essere una sessione di debug lunga.
Dal momento che è frequente che una modifica apportata a una classe interrompa qualcosa in un'altra, è possibile che
sia necessario rieseguire non solo i test della classe modificata, ma molti altri. Idealmente, la suite completa di
test relativa al proprio componente viene rieseguita molte volte l'ora. Ogni volta che si apporta una modifica
significativa, la suite viene rieseguita, si esaminano i risultati e si passa alla modifica successiva o si corregge
l'ultima modifica. Si consiglia di impegnarsi nel rendere possibile tale feedback rapido.
Automatizzazione dei test
L'esecuzione di test spesso non è pratica, se i test sono manuali. Per alcuni componenti, i test automatizzati sono
semplici. Un esempio potrebbe essere un database all'interno della memoria. Esso comunica ai relativi client tramite
un'API e non dispone di alcuna interfaccia esterna. In tale caso, i test saranno simili ai seguenti:
/* Verificare che sia possibile aggiungere gli elementi al massimo una volta. */
// Impostazione
Database db = new Database();
db.add("key1", "value1");
// Verifica
boolean result = db.add("key1", "another value");
expect(result == false);
I test sono differenti dal codice client ordinario solo in un modo: invece di considerare certi i risultati delle
chiamate API, essi li verificano. Se l'API rende la scrittura del codice client semplice, rende semplice anche la
scrittura del codice di test. Se la scrittura del codice del test non è semplice, l'utente ha ricevuto
un'avvertenza che è possibile migliorare l'API. Test-first design è quindi coerente con la capacità del RUP (Rational
Unified Process) di incentrarsi sui rischi importanti in anticipo.
Tuttavia, più il componente è connesso in modo saldo al mondo esterno e più difficile sarà l'esecuzione del test. I due
casi più comuni sono le GUI (graphical user interface) e i componenti di back-end.
GUI (Graphical user interface)
Si supponga che il database nel suddetto esempio riceva i dati tramite una callback da un oggetto dell'interfaccia
utente. La callback viene chiamata quando l'utente compila determinati campi e preme un pulsante. La verifica di quanto
appena descritto compilando manualmente i campi e premendo un pulsante non è una pratica da svolgere molte volte in
un'ora. E' necessario trovare un modo per consegnare l'input sotto un controllo programmatico, generalmente premendo il
pulsante nel codice.
Se si preme il pulsante, una parte del codice nel componente verrà eseguita. Più probabilmente, tale codice modifica lo
stato di alcuni oggetti dell'interfaccia utente. Quindi occorre trovare un modo per interrogare tali oggetti in maniera
programmatica.
Componenti back-end
Si supponga che il componente da verificare non implementi un database. Al contrario, è un wrapper intorno a un
database reale su disco. La verifica rispetto a tale database potrebbe essere difficoltosa. Potrebbe essere difficile
eseguire l'installazione e la configurazione. Le licenze potrebbero essere costose. E' possibile che il database
rallenti i test, rendendone difficile l'esecuzione frequente. In tali casi, si consiglia di liberare il database con un
componente più semplice che supporti abbastanza i test.
Gli stub sono utili anche quando non è pronto un componente con il quale interagisce il proprio componente. Non è
auspicabile attendere il codice di qualcun altro per effettuare la verifica sul proprio.
Per ulteriori informazioni, consultare Concetto: Stub.
Non scrivere dei tool propri
La verifica dello sviluppatore è abbastanza semplice. Vengono impostati alcuni oggetti, si effettua una chiamata
tramite un'API, si verificano i risultati e, qualora non siano quelli previsti, si annuncia un errore di test. Si
consiglia inoltre di raggruppare i test in modo da poterli eseguire singolarmente o come suite complete. Gli strumenti
che supportano tali requisiti vengono denominati framework di test.
La verifica dello sviluppatore è semplice e i requisiti per i framework del test non sono complicati. Se,
tuttavia, si cede alla tentazione di scrivere il proprio framework di test, si impiegherà molto più tempo del previsto
nel tentativo di correggere il framework. Sono disponibili numerosi framework, commerciali e di origine aperta e non
esiste alcuna ragione per non utilizzarli.
Creare un codice di supporto
Il codice di test tende ad essere ripetitivo. E' frequente vedere sequenze di codice simili alla seguente:
// nome null non consentito
retval = o.createName("");
expect(retval == null);
// spazi iniziali non consentiti
retval = o.createName(" l");
expect(retval == null);
// spazi finali non consentiti
retval = o.createName("name ");
expect(retval == null);
// il primo carattere potrebbe non essere numerico
retval = o.createName("5allpha");
expect(retval == null);
Questo codice viene creato copiando un controllo, incollandolo e quindi modificandolo per effettuare un altro
controllo.
In questo caso, il pericolo è duplice. Se l'interfaccia viene modificata, occorrerà effettuare una modifica
consistente. (In casi più complicati, una semplice sostituzione globale non sarà sufficiente). Inoltre, se il codice è
complicato, è possibile che l'intento del test venga perso nel testo.
Quando si percepisce che sono presenti delle ripetizioni, considerare la possibilità di eliminare la ripetizione nel
codice di supporto. Anche se il precedente codice è un semplice esempio, è più leggibile e gestibile se scritto in
questo modo:
void expectNameRejected(MyClass o, String s) {
Object retval = o.createName(s);
expect(retval == null); }
...
// nome null non consentito
expectNameRejected(o, "");
// spazi iniziali non consentiti.
expectNameRejected(o, " l");
// spazi finali non consentiti.
expectNameRejected(o, "name ");
// il primo carattere potrebbe non essere numerico.
expectNameRejected(o, "5alpha");
Gli sviluppatori che scrivono i test spesso sbagliano nell'effettuare troppe operazioni di Copia e Incolla. Qualora ci
si dovesse accorgere di tale tendenza, è utile andare nell'altra direzione. Eliminare dal proprio codice tutto il testo
duplicato.
Scrivere prima i test
La scrittura dei test dopo il codice è un lavoro difficile. L'esortazione è impiegare il minor tempo possibile, finire
rapidamente e passare oltre. La scrittura dei test prima del codice rende la verifica parte di un loop di feedback
positivo. Man mano che si implementa ulteriore codice, i test passano finché non si arriva all'ultimo e il gioco è
fatto. Coloro che scrivono prima i test riescono maggiormente nell'impresa e non impiegano più tempo. Per ulteriori
informazioni sulla scrittura preventiva di tali test, consultare Concetto:
Test-first Design
Rendere i test comprensibili
Occorre prevedere che sia necessario modificare i test in un secondo momento. Una situazione tipica si verifica ad
esempio quando una successiva iterazione chiama una modifica alla funzionalità del componente. Come semplice esempio,
si supponga che il componente abbia dichiarato una volta un metodo radice quadrata come il seguente:
double sqrt(double x);
In tale versione, un argomento negativo ha causato la restituzione di NaN da parte di sqrt ("non un numero" da
IEEE 754-1985 Standard for Binary Floating-Point Arithmetic). Nella nuova iterazione, il metodo radice quadrata
accetterà numeri negativi e restituirà un risultato complesso:
Complex sqrt(double x);
I test obsoleti per sqrt dovranno cambiare. Ciò significa la comprensione di tale attività e il relativo
aggiornamento in modo che tali test vengano eseguiti con il nuovo sqrt. Durante l'aggiornamento dei test,
occorre attenzione a non intaccare la relativa capacità di rilevazione bug. Talvolta si verifica quanto segue:
void testSQRT () {
// Aggiornare queste verifiche per Complex
// Quando ho tempo -- bem
/* double result = sqrt(0.0); ... */ }
Altri modi sono più sottili: i test vengono modificati in modo da potere essere eseguiti, ma non testano più gli
elementi che dovevano testare in origine. Il risultato finale, dopo numerose iterazioni, potrebbe essere una suite di
test troppo debole per cogliere eventuali bug. Ciò viene spesso definito come "decadimento" della suite di test. Una
suite decaduta verrà abbandonata, perché non vale la pena proseguire.
Non è possibile mantenere la capacità di rilevazione bug di un test a meno che non sia chiaro quali idee del test vengono implementate dal test. Il codice del test tende a non avere
molti commenti, sebbene sia spesso difficile comprendere la ragione di ciò rispetto al codice del prodotto.
Il decadimento della suite di test avviene meno frequentemente nei test diretti di sqrt piuttosto che nei test
indiretti. Sarà presente un codice che chiama sqrt. Tale codice disporrà di test. Quando sqrt viene
modificato, alcuni di tali test non verranno completati correttamente. La persona che modifica sqrt dovrà
probabilmente modificare tali test. Dal momento che ha meno familiarità con essi e la loro relazione con la modifica è
meno chiara, dovrà ridurli anziché lasciarli passare.
Occorre prestare attenzione quando si crea un codice di supporto per i test (come indicato in precedenza): tale codice
dovrebbe chiarificare e non celare lo scopo dei test che lo utilizzano. Una lamentela comune relativa ai programmi OO
(object-oriented) è che non è presente alcuna ubicazione in cui effettuare le operazioni. Se si esamina un metodo,
tutto quello che appare evidente è che inoltra il relativo lavoro in un'altra ubicazione. Tale struttura presenta dei
vantaggi, ma rende più difficile la comprensione del codice per le nuove persone. A meno che non venga profuso un
impegno, le modifiche risulteranno probabilmente non corrette o renderanno il codice ancora più complicato e fragile.
Lo stesso avviene per il codice di test, con l'eccezione che coloro che lo gestiranno in un secondo momento saranno
meno propensi a prestarvi la dovuta attenzione. Per evitare tale problema, si consiglia di scrivere test comprensibili.
Stabilire una corrispondenza tra le struttura del test e la struttura del prodotto
Si supponga che qualcuno abbia ereditato un componente. Sarà necessario modificarne una parte. E' possibile che questa
persona desideri esaminare i test precedenti come supporto per la nuova progettazione. Probabilmente desidererà
aggiornare i test precedenti prima di scrivere il codice (test-first design).
Tutte le suddette buone intenzioni non potranno produrre alcun risultato se non vengono individuati i test appropriati.
La suddetta persona non farà altro che apportare la modifica, individuare i test che non vengono completati
correttamente e correggerli. Ciò contribuirà al decadimento della suite di test.
Per tale ragione, è importante che la suite di test sia bene strutturata e che l'ubicazione di tali test sia
prevedibile dalla struttura del prodotto. Generalmente, gli sviluppatori organizzano i test in una gerarchia parallela,
con una classe di test per classe di prodotti. Quindi, se qualcuno modifica una classe denominata Log, sarà
che la classe di test è TestLog e saprà anche dove trovare il file sorgente.
Consentire ai test di violare l'incapsulamento
E' possibile limitare i test all'interazione con il proprio componente esattamente come avviene con il codice client,
tramite la stessa interfaccia utilizzata dal codice client. Tuttavia, ciò presenta degli svantaggi. Si supponga di
testare una semplice classe che gestisce un elenco con doppio collegamento:
Fig1: elenco con doppio collegamento
In particolare, si sta testando il metodo DoublyLinkedList.insertBefore(Object existing, Object newObject). In
uno dei test, si desidera inserire un elemento al centro dell'elenco, quindi verificare se è stato inserito
correttamente. Il test utilizza il suddetto elenco per creare questo elenco aggiornato:
Fig2: Elenco con doppio collegamento - voce inserita
Esso verifica la correttezza dell'elenco, in questo modo:
// l'elenco contiene un elemento in più.
expect(list.size()==3);
// il nuovo elemento si trova nella posizione corretta
expect(list.get(1)==m);
// controllare che altri elementi si trovano ancora lì.
expect(list.get(0)==a); expect(list.get(2)==z);
Ciò sembra sufficiente, ma non lo è. Si supponga che l'implementazione dell'elenco non sia corretta e che i puntatori
all'indietro non vengano impostati correttamente. Ossia, si supponga che l'elenco aggiornato sia simile al seguente:
Fig3: Elenco con doppio collegamento - errore nell'implementazione
Se DoublyLinkedList.get(int index) attraversa l'elenco dall'inizio alla fine (probabile), il test non coglierà
tale errore. Se la classe fornisce i metodi elementBefore e elementAfter, il controllo di tali errori
sarà semplice:
// Controllare che i link sono tutti aggiornati
expect(list.elementAfter(a)==m);
expect(list.elementAfter(m)==z);
expect(list.elementBefore(z)==m);
//questo genera un errore
expect(list.elementBefore(m)==a);
Cosa accade se tali metodi non vengono forniti? E' possibile pianificare sequenze più elaborate delle chiamate al
metodo, che avranno esito negativo se il difetto sospettato è presente. Ad esempio, scrivere quanto segue:
// Controllare se il collegamento all'indietro, da Z, è corretto.
list.insertBefore(z, x);
// Se è stato aggiornato in modo non corretto, X sarà
// inserito subito dopo la A.
expect(list.get(1)==m);
Ma tale test è più difficile da creare e ancor più difficile da gestire. (A meno che non vengano scritti dei commenti
esaurienti, non sarà chiaro perché il test prevede l'esecuzione di determinate azioni). Esistono due soluzioni:
-
Aggiungere i metodi elementBefore e elementAfter all'interfaccia pubblica. Ma ciò espone
l'implementazione a chiunque e rende le future modifiche più difficili.
-
Lasciare che i test conducano un'analisi della situazione dalle fondamenta e controllino direttamente i puntatori.
L'ultima è generalmente la migliore soluzione, anche per una semplice classe come DoublyLinkedList e
specialmente per le classi più complesse che si verificano nei prodotti.
Generalmente, i test vengono inseriti nello stesso pacchetto della classe che testano. Viene loro concesso accesso
protetto o friend.
Errori tipici di progettazione dei test
Ogni test viene esercitato su un componente e controlla i risultati corretti. La progettazione del test (gli input
utilizzati e le modalità di controllo della correttezza) può essere utile per rilevare i difetti o potrebbe nasconderli
inavvertitamente. Di seguito vengono riportati alcuni errori caratteristici di progettazione dei test.
Errore nello specificare i risultati previsti in anticipo
Si supponga di dovere testare un componente che converte gli XML in HTML. La tentazione è prendere alcuni XML di
esempio, eseguirli attraverso la conversione e visualizzare i risultati in un browser. Se la schermata è corretta, si
salva l'HTML come risultato previsto ufficiale. In un secondo momento, un test confronta l'output reale della
conversione con i risultati previsti.
Tale pratica è rischiosa. Anche gli utenti di computer sofisticati prendono per buone le operazioni del computer. E'
probabile che gli errori sfuggano nell'aspetto della schermata. (Senza trascurare che i browser tollerano abbastanza
gli HTML non formattati correttamente). Considerando l'HTML non corretto come risultato previsto ufficiale, ci si
accerta che il test non rileverà mai il problema.
E' meno rischioso, ma comunque lo è, eseguire un duplice controllo esaminando direttamente l'HTML. Dal momento che
l'output è complicato, sarà semplice trascurare gli errori. Per rilevare più difetti, occorrerà scrivere prima a mano
l'output previsto.
Errore nel controllare il background
I test generalmente controllano che siano avvenute le modifiche previste, ma i relativi creatori spesso dimenticano di
controllare che gli elementi che non andavano modificati siano rimasti tali e quali. Ad esempio, si supponga che un
programma debba modificare i primi 100 record in un file. E' bene controllare che il 101esimo non sia stato
modificato.
In teoria, bisognerebbe controllare che nulla nel background sia stato escluso: l'intero file system, tutta la memoria,
ogni elemento raggiungibile tramite la rete. In pratica, è necessario scegliere attentamente gli elementi che ci si può
permettere di controllare. Ma è importante fare tale scelta.
Errore nel controllare la persistenza
Il solo fatto che il componente indichi che è stata apportata una modifica, non significa che questa sia stata
confermata al database. Occorre controllare il database in un altro modo.
Errore nell'aggiungere la varietà
E' possibile che un test venga progettato per controllare l'effetto di tre campi in un record del database ma, per
eseguire il test, occorre compilare molti altri campi. I tester utilizzeranno spesso e ripetutamente gli stessi valori
per questi campi "irrilevanti". Ad esempio, utilizzeranno sempre il nome del proprio fidanzato/a in un campo di testo o
999 in un campo numerico.
Il problema è che, talvolta, ciò che non dovrebbe avere importanza la ha. Sempre più frequentemente, è presente un bug
che dipende da una qualche oscura combinazione di input improbabili. Se si utilizzano sempre gli stessi input, non vi è
alcuna possibilità di individuare tali bug. Se si cambiano continuamente gli input, tale possibilità diventa più reale.
Molto spesso, non costa nulla utilizzare un numero diverso da 999 o utilizzare il nome di qualcun altro. Quando il
cambiamento dei valori utilizzati nei test non costa praticamente nulla e presenta notevoli vantaggi potenziali, è
opportuno effettuarlo. (Nota: non è consigliabile utilizzare i nomi dei precedenti fidanzati invece di quello attuale,
se il fidanzato attuale lavora con voi).
Ecco un altro vantaggio. Un errore plausibile per il programma consiste nell'utilizzare il campo X invece del
campo Y. Se entrambi i campi contengono "Dawn", non è possibile individuare l'errore.
Errore nell'utilizzo di dati realistici
E' frequente l'utilizzo di dati inventati nei test. Spesso tali dati sono stranamente semplici. Ad esempio, i nomi di
clienti potrebbero essere "Mickey", "Snoopy" e "Donald". Dal momento che i dati sono diversi da quelli immessi dagli
utenti reali, ad esempio più brevi, essi potrebbero impedire la rilevazione di difetti da parte degli utenti reali. Ad
esempio, questi nomi di una sola parola non rileveranno che il codice non gestisce i nomi con gli spazi.
E' prudente sforzarsi di utilizzare dati realistici.
Errore nel notare che il codice non produce alcuna operazione
Si supponga di inizializzare un record database a zero, eseguire un calcolo che dà come risultato lo zero memorizzato
nel record, quindi controllare che il record sia zero. Cosa ha dimostrato il test? E' possibile che il calcolo non sia
stato affatto eseguito. E' possibile che non sia stato memorizzato alcun elemento e il test non lo ha rilevato.
L'esempio è alquanto improbabile. Ma questo stesso errore potrebbe essere riprodotto in modi più sottili. Ad esempio,
si potrebbe scrivere un test per un complicato programma di installazione. Il test ha l'obiettivo di controllare che
tutti i file temporanei vengano rimossi in seguito a un'installazione completata correttamente. Ma, a causa di tutte le
opzioni del programma di installazione, uno specifico file temporaneo non è stato creato. Quasi sicuramente, è il file
che il programma ha dimenticato di rimuovere.
Errore nel notare che il codice produce l'operazione sbagliata
A volte un programma esegue l'azione corretta per le ragioni sbagliate. Come esempio banale, considerare il seguente
codice:
if (a < b && c)
return 2 * x;
else
return x * x;
L'espressione logica è sbagliata ed è stato scritto un test che la calcola in modo non corretto fornendole il ramo
sbagliato. Sfortunatamente, per pura coincidenza, la variabile X ha il valore 2 in tale test. Quindi, il risultato del
ramo sbagliato è accidentalmente corretto, uguale al risultato che avrebbe dato il ramo corretto.
Per ogni risultato previsto, bisognerebbe chiedersi se esiste un modo plausibile in cui tale risultato potrebbe essere
stato acquisito per la ragione sbagliata. Sebbene sia spesso impossibile saperlo, in alcuni casi non lo è.
|