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).
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.
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.
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).
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:
-
Quanto lavoro occorrerà per correggere questo test, magari aggiungendolo alla libreria del programma di utilità?
-
Come potrebbe essere utilizzato il tempo altrimenti?
-
Quante probabilità esistono che il test rilevi difetti seri in futuro? Qual è stato il record di traccia del test e
dei test correlati?
-
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).
|