Nel mondo reale, è perfettamente normale scrivere test unitari per il codice di qualcun altro. Certo, lo sviluppatore originale avrebbe dovuto farlo già, ma spesso si riceve codice legacy dove questo non è stato fatto. A proposito, non importa se quel codice legacy è arrivato decenni fa da una galassia molto lontana, o se uno dei tuoi colleghi l'ha controllato la settimana scorsa, o se l'hai scritto oggi, il codice precedente è codice senza test
Chiediti: perché scriviamo test unitari? Going Green è ovviamente solo un mezzo per un fine, l'obiettivo finale è quello di dimostrare o confutare le affermazioni sul codice in prova.
Supponiamo che tu abbia un metodo che calcola la radice quadrata di un numero in virgola mobile. In Java, l'interfaccia lo definirebbe come:
public double squareRoot(double number);
Non importa se hai scritto l'implementazione o se qualcuno l'ha fatto, vuoi affermare alcune proprietà di squareRoot:
- che può restituire radici semplici come sqrt (4.0)
- che può trovare una vera radice come sqrt (2.0) con una precisione ragionevole
- che trova che sqrt (0.0) è 0.0
- che genera un IllegalArgumentException quando viene alimentato un numero negativo, ad esempio su sqrt (-1.0)
Quindi inizi a scriverle come test individuali:
@Test
public void canFindSimpleRoot() {
assertEquals(2, squareRoot(4), epsilon);
}
Oops, questo test ha già esito negativo:
java.lang.AssertionError: Use assertEquals(expected, actual, delta) to compare floating-point numbers
Hai dimenticato l'aritmetica in virgola mobile. OK, introduci double epsilon=0.01
e vai:
@Test
public void canFindSimpleRootToEpsilonPrecision() {
assertEquals(2, squareRoot(4), epsilon);
}
e aggiungi gli altri test: finalmente
@Test
@ExpectedException(IllegalArgumentException.class)
public void throwsExceptionOnNegativeInput() {
assertEquals(-1, squareRoot(-1), epsilon);
}
e oops, di nuovo:
java.lang.AssertionError: expected:<-1.0> but was:<NaN>
Avresti dovuto provare:
@Test
public void returnsNaNOnNegativeInput() {
assertEquals(Double.NaN, squareRoot(-1), epsilon);
}
Che cosa abbiamo fatto qui? Abbiamo iniziato con alcune ipotesi su come il metodo dovrebbe comportarsi e abbiamo scoperto che non tutte erano vere. Abbiamo quindi creato la suite di test Green, per dimostrare che il metodo si comporta in base alle nostre ipotesi corrette. Ora i client di questo codice possono fare affidamento su questo comportamento. Se qualcuno dovesse scambiare l'effettiva implementazione di squareRoot con qualcos'altro, qualcosa che per esempio ha davvero lanciato un'eccezione invece di restituire NaN, i nostri test l'avrebbero catturato immediatamente.
Questo esempio è banale, ma spesso si ereditano grandi pezzi di codice in cui non è chiaro cosa faccia effettivamente. In tal caso, è normale posizionare un cablaggio di test attorno al codice. Inizia con alcune ipotesi di base su come dovrebbe comportarsi il codice, scrivi test unitari per loro, prova. Se verde, bene, scrivi altri test. Se rosso, beh ora hai un'affermazione fallita che puoi tenere contro una specifica. Forse c'è un bug nel codice legacy. Forse le specifiche non sono chiare su questo particolare input. Forse non hai una specifica. In tal caso, riscrivi il test in modo che documenti il comportamento imprevisto:
@Test
public void throwsNoExceptionOnNegativeInput() {
assertNotNull(squareRoot(-1)); // Shouldn't this fail?
}
Nel corso del tempo, si finisce con un'imbracatura di test che documenta come si comporta effettivamente il codice e diventa una sorta di specifica codificata. Se si desidera modificare il codice legacy o sostituirlo con qualcos'altro, si dispone del cablaggio di prova per verificare che il nuovo codice si comporti allo stesso modo o che il nuovo codice si comporti diversamente in modi previsti e controllati (ad esempio che in realtà corregge il bug che ci si aspetta che risolva). Questa imbracatura non deve essere completa il primo giorno, infatti, avere un'imbracatura incompleta è quasi sempre meglio che non avere alcuna imbracatura. Avere un'imbracatura significa poter scrivere il codice cliente con maggiore facilità, sapere dove aspettarsi che le cose si rompano quando si cambia qualcosa e dove si sono spezzate quando alla fine l'hanno fatto.
Dovresti cercare di uscire dalla mentalità che devi scrivere dei test unitari solo perché devi, come riempire i campi obbligatori di un modulo. E non dovresti scrivere test unitari solo per rendere verde la linea rossa. I test unitari non sono tuoi nemici, i test unitari sono i tuoi amici.