Concetto: Verifica dello sviluppatore
Questa linea guida fornisce dei suggerimenti per superare i primi ostacoli durante la creazione dei test dello sviluppatore e di una suite di test gestibile per tutto il progetti. Vengono inoltre forniti consigli per la creazione di test dello sviluppatore migliori.
Relazioni
Elementi correlati
Descrizione principale

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:

Immagine di esempio dell'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:

Immagine di esempio dell'elenco con doppio collegamento e con voce inserita

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:

Immagine di esempio dell'elenco con doppio collegamento e con errore di implementazione

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:

  1. Aggiungere i metodi elementBefore e elementAfter all'interfaccia pubblica. Ma ciò espone l'implementazione a chiunque e rende le future modifiche più difficili.
  2. 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 è.