Linea guida: Gestione di suite di test automatizzate
Questa linea guida presenta i principi di progettazione e di gestione che facilitano la manutenzione delle suite di test.
Relazioni
Descrizione principale

Introduzione

Come avviene per gli oggetti fisici, i test possono essere interrotti. Il motivo non è perché diventano obsoleti, ma semplicemente perché qualcosa è cambiato nel loro ambiente. E' possibile che siano stati trasferiti in un nuovo sistema operativo. O, più probabilmente, il codice che verificano è stato modificati in un modo che provoca correttamente l'esito negativo del test. Si supponga di lavorare su una versione 2.0 di un'applicazione di e-banking. Nella versione 1.0, veniva utilizzato il seguente metodo per la registrazione:

public boolean login (String username);

Nella versione 2.0, il reparto di marketing ha realizzato che la protezione della password potrebbe essere una buona idea. Quindi il metodo è cambiato in questo modo:

public boolean login (String username, String password);

Qualsiasi test che utilizza il login non riuscirà e non potrà nemmeno compilarsi. Dal momento che non è possibile effettuare alcuna operazione a questo punto, non è possibile scrivere molti test utili senza login Si potrebbe avere a che fare con centinaia o migliaia di test in errore.

E' possibile correggere tali test utilizzando un tool di search-and-replace (ricerca e sostituzione) globale che rilevi ogni istanza di login(qualcosa) e lo sostituisca con login(qualcosa, "password fittizia"). Quindi fare in modo che tutti gli account di verifica utilizzino tale password e il gioco è fatto.

Quindi, quando il marketing deciderà che le password non possono contenere spazi, occorrerà rifare tutto il lavoro da capo.

Questo tipo di situazione è un fardello devastante, specialmente quando, come avviene frequentemente, le modifiche al test non vengono apportate così facilmente. Ma esiste un modo migliore.

Si supponga che i test, in origine, non chiamassero il metodo di login del prodotto. Piuttosto, essi chiamavano un metodo libreria che eseguiva tutte le azioni necessarie per registrare il test e renderlo pronto a procedere. Inizialmente, il metodo sarebbe stato simile al seguente:

public boolean testLogin (String username) {
  return product.login(username);
}

Quando si passa alla versione 2.0, la libreria del programma di utilità viene modificata per corrispondere a:

public Boolean testLogin (String username) {
  return  product.login(username

, "dummy password");
}

Invece di modificare un migliaio di test, viene modificato il metodo.

Idealmente, tutti i metodi libreria necessari saranno disponibili all'inizio dell'impegno della verifica. In pratica, non potranno essere anticipati tutti; potrebbe non essere evidente che è necessario un metodo testLogin del programma di utilità fino alla prima volta in cui il login del prodotto cambia. Quindi i metodi dei programmi di utilità di un test sono spesso scomposti da test esistenti, secondo le necessità. E' estremamente importante eseguire questa correzione del test continua, anche sotto la pressione della pianificazione. In caso contrario, si impiegherà molto tempo nella gestione di una suite di test spiacevole e ingestibile. E' possibile che si opti per l'eliminazione di tale suite o che ci si renda conto dell'impossibilità di scrivere tutti i nuovi test necessari, perché il tempo di verifica disponibile è stato impiegato nella gestione dei vecchi.

Nota: i test del metodo di login del prodotto continuerà a chiamarlo direttamente. Se la relativa funzionalità cambia, sarà necessario aggiornare alcuni o tutti i test. (Se nessuno dei test di login ha esito negativo quando la relativa funzionalità viene modificata, non sono così efficaci nella rilevazione dei difetti).

L'astrazione consente la gestione della complessità

Il precedente esempio ha mostrato quanti test possono allontanarsi dall'applicazione reale. Molto probabilmente, è possibile pensare a situazioni molto più astratte. Si potrebbe rilevare che diversi test iniziano con una sequenza comune di chiamate al metodo: dopo la registrazione, configurano uno stato e si dirigono verso la parte dell'applicazione che si sta testando. Soltanto in seguito ogni test effettua operazioni differenti. Tutta la suddetta configurazione potrebbe, e dovrebbe, essere racchiusa in un singolo metodo con nome evocativo, ad esempio readyAccountForWireTransfer. Effettuando tale operazione, si risparmia un considerevole lasso di tempo durante la scrittura di un particolare tipo di test e si rende, inoltre, più comprensibile l'intento di ogni test.

E' importante che i test siano comprensibili. Un problema comune con le suite di test obsolete è che nessuno sa cosa stanno facendo i test e perché. Quando vengono interrotti, la tendenza è correggerli nel modo più semplice possibile. Ciò rende spesso i test meno efficaci nella rilevazione dei difetti. Essi non testano più gli elementi per cui erano stati progettati.

Un altro esempio

Si supponga di dover testare un compilatore. Alcune delle prime classi scritte definiscono la struttura di analisi interna del compilatore e le trasformazioni apportate ad essa. Sono disponibili diversi test che costruiscono strutture di analisi e testano le trasformazioni. Uno di tali test potrebbe essere simile al seguente:

/*
 * Specificato
 *   mentre (i<0) { f(a+i); i++;}
 * "a+i" non può essere sollevato dal loop perché
 * contiene una variabile modificata nel loop.
 */
loopTest = new LessOp(new Token("i"), new Token("0"));
aPlusI = new PlusOp(new Token("a"), new Token("i"));
statement1 = new Statement(new Funcall(new Token("f"), aPlusI));
statement2 = new Statement(new PostIncr(new Token("i"));
loop = new While(loopTest, new Block(statement1, statement2));
expect(false, loop.canHoist(aPlusI))

Questo è un test di difficile lettura. Si supponga che il tempo passi. Avvengono dei cambiamenti che richiedono l'aggiornamento dei test. A questo punto, si dispone di ulteriore infrastruttura del prodotto da utilizzare. In particolare, è possibile che si disponga di una routine di analisi che converte le stringhe in strutture di analisi. A questo punto, sarebbe meglio riscrivere completamente i test per utilizzarla:

loop=Parser.parse("while (i<0) { f(a+i); i++; }");
// Acquisire un puntatore nella parte "a+i" del loop. 
aPlusI = loop.body.statements[0].args[0];
expect(false, loop.canHoist(aPlusI));

Tali test saranno molto più semplici da comprendere e consentiranno di risparmiare tempo immediatamente e in futuro. Infatti, i relativi costi di gestione sono talmente bassi che potrebbe essere una buona idea rimandare la maggior parte di essi finché il programma di analisi sarà disponibile.

Questo approccio presenta un lato leggermente negativo: tali test potrebbero rilevare un difetto nel codice di trasformazione (come previsto) o nel programma di analisi (per caso). Di conseguenza, il debug e l'isolamento del problema potrebbero essere, in qualche modo, difficili. Dall'altro lato, la rilevazione di un problema che non è stato individuato dal programma di analisi non è poi un aspetto così negativo.

Esiste anche la possibilità che un difetto nel programma di analisi mascheri un difetto nel codice di trasformazione. Tale possibilità è abbastanza remota e il costo che ne deriva è quasi certamente inferiore al costo di gestione di test più complicati.

Analisi approfondita del miglioramento di un test

Una suite di test di grandi dimensioni conterrà alcuni blocchi di test che non cambiano. Essi corrispondono ad aree stabili nell'applicazione. Altri blocchi di test cambieranno con frequenza. Essi corrispondono ad aree nell'applicazione in cui la funzionalità è in costante cambiamento. Questi ultimi blocchi di test tenderanno a fare un utilizzo cospicuo delle librerie dei programmi di utilità. Ogni test testerà funzionalità specifiche nell'area modificabile. Le librerie del programma di utilità sono progettate per consentire che tale test controlli le funzionalità di destinazione rimanendo relativamente immune alle modifiche nelle funzionalità non testate.

Ad esempio, il test di "loop hoisting", mostrato in precedenza, è ora immune ai dettagli della modalità di creazione delle strutture di analisi. E' ancora sensibile alla struttura di una struttura di analisi loop while (a causa delle sequenze di accessi richieste per recuperare la struttura secondaria per a+i). Se tale struttura si rivela modificabile, i test possono essere resi più astratti tramite la creazione di un metodo del programma di utilità fetchSubtree :

loop=Parser.parse("while (i<0) { f(a+i); i++; }");


aPlusI = fetchSubtree(loop, "a+i");

expect(false, loop.canHoist(aPlusI));

Il test è ora sensibile a due soli elementi: la definizione del linguaggio (ad esempio, i numeri interi possono essere incrementati con ++) e le regole che stabiliscono il loop hoisting (la funzionalità di cui si sta controllando la correttezza).

Eliminazione dei test

Anche con le librerie dei programmi di utilità, un test può essere interrotto periodicamente da modifiche di funzionalità che non hanno nulla a che vedere con gli elementi controllati. La correzione del test non garantisce affatto la rilevazione del difetto dovuto alla modifica; è un'azione che viene eseguita per preservare la possibilità che il test, in futuro, rilevi qualche altro difetto. Ma il costo di questa serie di correzioni potrebbe superiore alla possibilità ipotetica che il test rilevi un difetto. Potrebbe essere meglio eliminare il test e dedicare il proprio impegno a crearne di nuovi più validi.

La maggior parte delle persone resistono alla tentazione di eliminare un test, almeno finché non sono così sopraffatte dal peso della gestione che eliminano tutti i test. E' molto meglio prendere le decisioni con coscienza e continuità, test per test, chiedendosi:

  1. Quanto lavoro occorrerà per correggere questo test, magari aggiungendolo alla libreria del programma di utilità?
  2. Come potrebbe essere utilizzato il tempo altrimenti?
  3. Quante probabilità esistono che il test rilevi difetti seri in futuro? Qual è stato il record di traccia del test e dei test correlati?
  4. Quanto tempo trascorrerà prima che il test venga nuovamente interrotto?

Le risposte a queste domande saranno delle mere stime o ipotesi. Ma il porsi tali domande comporta risultati che vanno ben oltre la semplice rilevazione di un criterio di correzione di tutti i test.

Un'altra ragione per l'eliminazione dei test è la relativa ridondanza. Ad esempio, nella parte iniziale dello sviluppo, potrebbero esser presenti diversi test semplici di metodi di creazione strutture di analisi basilari (il costruttore LessOp e simili). In seguito, durante la scrittura del programma di analisi, esisteranno numerosi test del programma di analisi. Dal momento che tale programma utilizza i metodi di costruzione, i test li verificheranno indirettamente. Non appena le modifiche al codice provocano l'interruzione dei test di costruzione, si consiglia di eliminare alcuni perché ridondanti. Naturalmente, qualsiasi funzionalità di costruzione nuova o modificata richiederà nuovi test. Tali test potrebbero essere implementati direttamente (se è difficile eseguirli tramite il programma di analisi) o indirettamente (se i test tramite il suddetto programma sono adeguati e più gestibili).