TDD-Backfire

E’ mezzanotte e un quarto e ho trovato il baco.
Ho iniziato alle 10.30.
3 pomodori buttati !

Ecco perchè :

Oggi, verso il finire della giornata di lavoro, il mio compagno di pair doveva andarsene a casa, io volevo vedere spuntare il risultato, insomma, integrare tutto prima di andarmene.
Gli ho detto che mi facevo un ultimo pomodoro da solo e ho persino scherzato : “E così riuscirò a inserire tre bug!”

Tre, no, uno, si, ma è bastato.

Per poter vedere girare tutto bisognava completare il task : “Un test può avere molti risultati”
In particolare noi prima avevamo una lista di possibili controlli, ognuno di questi detto “Check”. Un Check lo si crea passandogli la descrizione del test da effettuare e la condizione che deve essere soddisfatta, pressapoco così :
new Check(“Click on ok”,”the result is 4″)

Il fatto è che ci potrà essere anche un check così :
new Check(“Click on ok”,”the result is 10″)

Il test da effettuare è sempre lo stesso, ma, cambiando altre condizioni, il risultato atteso è diverso.
Noi vogliamo assicurarci che eventuali tests già esistenti non vengano di fatto duplicati al momento della creazione di un nuovo Check che differisce solo nel risultato.
In altre parole vogliamo che ogni frase di test possa avere molteplici risultati, ogni coppia test-risultato diventa poi il nostro check.

Niente di più facile, le factory esistono per questo.
L’idea è questa, avere una classe DomainTest, che si istanzia così : new DomainTest(“Click on ok”);
poi con il test si possono creare tanti bei Check :
test.createCheck(“the result is 4”);
test.createCheck(“the result is 10”);

Lindo no?

Il TDD è sacro, quindi mi preoccupo di come testare la nuova funzionalità. Mi guardo attorno e noto questa e altre suite di test simili :
…..
Check check1 = new Check(“test 1″,”result 1”);
Check check2 = new Check(“test 2″,”result 2”);
Check check3 = new Check(“test 3″,”result 3”);

…..fai questo, fai quello…..

assertEquals(check1,action.getNextCheck());
assertEquals(check2,action.getNextCheck());
assertEquals(check3,action.getNextCheck());
assertNull(action.getNextCheck()); // End of checks is reported with a null, which is fair for Struts 2
…..

E mi dico : “Vorrai mica che con tutte queste inizializzazioni di Check mi metta a scrivere dei test aggiuntivi che controllino il nuovo modo di inizializzare check? Se qualcosa va storto di sicuro fallirà almeno uno di questi test”
Si apprezzi la furbizia; stavo aggiungendo una funzionalità, cosa che avrei dovuto fare solo in caso di test rosso, ma visto che avevo tanti bei test verdi che avrebbero usato quella funzionalità trasversalmente, ho deciso di concedermi di invertire il processo.

Passo quindi all’implementazione : creo una classe Test, poi vado su Check, prendo il costruttore e gli do un bel “introduce factory” con l’idea di spostarla poi in Test.
Eclipse mi sputa in un occhio, ogni tanto gli prende… vabbè, lo faccio a manina.
Metto a default il costruttore di Check, gli cambio il primo parametro da String a Test :
Check(DomainTest test, String result) …..

Subito balzano fuori dei bei rossi di compilazione, tutti test basati sui Check sono rotti, vado nella suite suddetta e ci scrivo la nuova forma di creazione dei check :
Check check1 = new Test(“test 1”).createCheck(“result 1”);
Manca il metodo createCheck, ctrl + 1 ed Eclipse me lo crea al volo, vuoto, poi passo a sostituire tutti i “new Check” come avevo fatto col primo.
E’ lunga quando il refactoring di Eclipse ti lascia a terra.

A quel punto era davvero tardi, convinto di aver finito lancio tutti i test, acceptance inclusa (in HtmlUnit) pronto ad andarmene a casa.
Rossi… ma solo 4 e solo tra quelli di acceptance, quelli che testavano le azioni che usavano i check sono tutti ok.

Bizzarro.. la modifica è sui Check, perchè non sono falliti i test unitari che usano quelli?

Scappo a casa.

Col tarlo nella testa alle 10 passate riapro il laptop… che cosa avevo combinato?

Guardo i fallimenti, e mi parlano di descrizioni di Check mancanti, ma, ovviamente, visto che i tests sui Check erano verdi l’ultima cosa che penso è di rileggere quelli. Inizio invece a farmi delle paranoie sull’inizializzazione delle actions con Spring, sulla sessione, se per caso si perdevano degli oggetti dentro al modello, se i nuovi checks venivano ben inseriti dalle azioni… ho persino aggiunto nuova acceptance per assicurarmi che le jsp facessero davvero quello che dicevano.

Per ognuna di queste paranoie ho scritto un test, e passavano tutti.

Finalmente, praticamente per sbaglio, giusto perchè tutte le cose ingarbugliate passavano senza problemi, ho aggiunto questo :
@Test
public void firstCheckExists() {
assertNotNull(action.getNextCheck());
}

Perchè dico “per sbaglio”… perchè quel controllo io lo consideravo implicito nelle suite di tests come quella che ho riassunto sopra.
L’ho aggiunto così.. attendendomi il verde, quasi per scaramanzìa.

In tutte le mie paranoie avevo sperato di vedere spuntare il rosso, ed era sempre tutto disperatamente verde.
E invece quello, due secondi di scrittura, un picosecondo di esecuzione, in cui davo per scontato il verde è diventato subito rosso.

Il maledetto Check era nullo. Ma nullo nullo.

Un po’ come un assiduo camminatore che inciampa nel giardino di casa propria e vuole rialzarsi mostrando di sapere ancora camminare; invece di andare subito ad aprire e modificare il metodo che ormai sapevo essere la causa di tutto, ho scritto il test che avrei dovuto scrivere da subito :
@Test
public void domainTestCreatesNewCheckWithGivenResult() {
DomainTest test1 = new DomainTest(“test 1”);
Check check1 = test1.createCheck(“result 1”);
assertNotNull(check1);
assertEquals(“test 1 : result 1”,check1.toString());
}

circa 20 secondi per scriverlo. Rosso. Ovviamente.

Eclipse, col ctrl + 1, aveva creato sì il metodo createCheck e lo aveva creato così :
public Check createCheck(String result) {
// TODO stub blah blah
return null;
}

Poi, dovendo andare ad aggiornare tutti le istanziazioni a mano mi ero dimenticato di implementare quel metodo. Non c’era il navigatore.

Ma perchè i miei test che usavano Check continuavano a passare?

Perchè la verità è che nessun test assicura la correttezza di una feature a meno che non sia stato creato proprio per quella feature. Sperare nel “cross-testing” significa tramutare la propria suite di tests, sviluppata con rigore in settimane di lavoro, da strumento di grande qualità, a fonte di false sicurezze, che azzoppano il processo di analisi.

I tests di cui sopra passavano tutti grazie a questo : assertEquals(null,null) e assertNull(null)

I costruttori non possono mai ritornare null, al massimo sganciano eccezioni, mentre le factory possono farlo tranquillamente. Il refactoring era incompleto, la modifica non era invariante del comportamento, e amen, ma il fatto è che mancavano quattro linee di test, quello è l’errore, punto.

Ci vuole così poco, un one-liner non testato… pensare che giusto qualche tempo fa avevo giusto risposto a un’osservazione con una frase molto Yoda : “Sono le responsabilità semplici che bisogna ricordarsi di testare, le altre te lo ricordano da sole”

La domanda, legittima, è anche : ma quando ho creato “Check” non ho testato i suoi metodi principali?

Già… qui bisogna ringraziare tanto la pratica del javabeaning selvaggio promosso da buona parte dei framework web per Java. Check di fatto è un elemento della Gui, ma invece di avere un metodo con la responsabilità di renderizzarlo, questa azione viene delegata alle jsp, così Check è solo un data-holder triste, che fa solo set e get dei suoi attributi.

I data-holder sono sbagliati non solo per i danni che causano alla divisione delle responsabilità tanto cara al design OO, ma anche perchè, subdoli, tendono a non farsi testare. In fin dei conti, se non hanno responsabilità c’è poco da testare.

Un ultimo appunto : quei “TODO” che mette eclipse nemmeno li vedo, forse è meglio passare i commenti degli stub a “FIXME” e tenere aperta la view.

Comments

ok, so che per te è stata una faticaccia, ma ne è uscito un post utile per noi lettori :)
Posted by riffraff on 05/06/2007 11:18:24 PM

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s