In TDD, se scrivo un caso di test che passa senza modificare il codice di produzione, cosa significa?


17

Queste sono le regole di Robert C. Martin per TDD :

  • Non è consentito scrivere alcun codice di produzione a meno che non si debba effettuare un test unit unit fallito.
  • Non è consentito scrivere più unit test di quanto sia sufficiente per fallire; e gli errori di compilazione sono errori.
  • Non è consentito scrivere più codice di produzione di quanto sia sufficiente per superare un test unitario non riuscito.

Quando scrivo un test che sembra utile ma che passa senza cambiare il codice di produzione:

  1. Significa che ho fatto qualcosa di sbagliato?
  2. Dovrei evitare di scrivere tali test in futuro se può essere aiutato?
  3. Devo lasciare quel test lì o rimuoverlo?

Nota: stavo cercando di porre questa domanda qui: posso iniziare con un test unitario di passaggio? Ma non sono stato in grado di articolare la domanda abbastanza bene fino ad ora.


Il "Bowling Game Kata" collegato all'articolo che citi ha in realtà un test che passa immediatamente come passaggio finale.
jscs

Risposte:


21

Dice che non è possibile scrivere il codice di produzione a meno che non si riesca a superare un test unitario non riuscito, non che non si possa scrivere un test che passa dall'inizio. L'intento della regola è di dire "Se è necessario modificare il codice di produzione, assicurarsi di scrivere o modificare prima un test per esso".

A volte scriviamo test per dimostrare una teoria. Il test ha esito positivo e ciò smentisce la nostra teoria. Non rimuoviamo quindi il test. Tuttavia, potremmo (sapendo che abbiamo il supporto del controllo del codice sorgente) rompere il codice di produzione, per assicurarci di capire perché è passato quando non ce lo aspettavamo.

Se risulta essere un test valido e corretto e non duplica un test esistente, lasciarlo lì.


Il miglioramento della copertura dei test del codice esistente è un altro motivo perfettamente valido per scrivere un test (si spera) di passaggio.
Jack,

13

Significa che:

  1. Hai scritto il codice di produzione che soddisfa la funzione che desideri senza scrivere prima il test (una violazione del "TDD religioso"), oppure
  2. La funzione di cui hai bisogno sembra essere già soddisfatta dal codice di produzione e stai solo scrivendo un altro test unitario per coprire quella funzione.

Quest'ultima situazione è più comune di quanto si possa pensare. Come esempio completamente specioso e banale (ma ancora illustrativo), diciamo che hai scritto il seguente test unitario (pseudocodice, perché sono pigro):

public void TestAddMethod()
{
    Assert.IsTrue(Add(2,3) == 5);
}

Perché tutto ciò di cui hai veramente bisogno è il risultato di 2 e 3 sommati.

Il tuo metodo di attuazione sarebbe:

public int add(int x, int y)
{
    return x + y;
}

Ma diciamo che ora devo aggiungere 4 e 6 insieme:

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

Non ho bisogno di riscrivere il mio metodo, perché copre già il secondo caso.

Ora diciamo che ho scoperto che la mia funzione Aggiungi ha davvero bisogno di restituire un numero che ha un certo limite, diciamo 100. Posso scrivere un nuovo metodo che testa questo:

public void TestAddMethod3()
{
    Assert.IsTrue(Add(100,100) == 100);
}

E questo test ora fallirà. Ora devo riscrivere la mia funzione

public int add(int x, int y)
{
    var a = x + y;
    return a > 100 ? 100 : a;
}

per farlo passare.

Il buon senso impone che se

public void TestAddMethod2()
{
    Assert.IsTrue(Add(4,6) == 10);
}

passa, non fai deliberatamente fallire il tuo metodo solo così puoi avere un test fallito in modo da poter scrivere un nuovo codice per far passare quel test.


5
Se seguissi completamente gli esempi di Martin (e non ti suggerisse necessariamente di farlo), per far add(2,3)passare, otterrai letteralmente 5. Hard-coded. Quindi scrivi il test per il add(4,6)quale ti costringerebbe a scrivere il codice di produzione che lo fa passare senza interrompere add(2,3)contemporaneamente. Si potrebbe finire con return x + y, ma non sarebbe iniziare con esso. In teoria. Naturalmente, a Martin (o forse era qualcun altro, non ricordo) piace fornire esempi di questo tipo per l'educazione, ma non si aspetta che tu scriva un codice così banale in quel modo.
Anthony Pegram,

1
@tieTYT, in generale, se ricordo bene dal libro (i) di Martin, il secondo caso di prova in genere sarebbe sufficiente per farti scrivere la soluzione generale per un metodo semplice (e, in realtà, in effetti, faresti semplicemente funzionare il prima volta). Non c'è bisogno di un terzo.
Anthony Pegram,

2
@tieTYT, allora continueresti a scrivere test fino a quando non lo farai. :)
Anthony Pegram,

4
C'è una terza possibilità, e va contro il tuo esempio: hai scritto un test duplicato. Se segui TDD "religiosamente", allora un nuovo test che passa è quindi sempre una bandiera rossa. Dopo DRY , non dovresti mai scrivere due test che testano essenzialmente la stessa cosa.
congusbongus,

1
"Se seguissi completamente gli esempi di Martin (e non ti suggerisce necessariamente di farlo), per fare il passaggio add (2,3), otterrai letteralmente 5. Hard-coded." - questo è un po 'di TDD rigoroso che mi ha sempre grattato, l'idea che tu scriva codice che sai è sbagliato nell'aspettarsi che un test futuro arrivi e lo provi. E se quel test futuro non fosse mai scritto, per qualche motivo, e i colleghi presumessero che "tutto-test-verde" implica "tutto-codice-corretto"?
Julia Hayward,

2

Il tuo test ha superato ma non sbagli. Penso che sia successo perché il codice di produzione non è TDD dall'inizio.

Supponiamo che il TDD canonico (?). Non esiste un codice di produzione ma alcuni casi di test (che ovviamente falliscono sempre). Aggiungiamo il codice di produzione da passare. Quindi fermati qui per aggiungere altri casi di test falliti. Aggiungi di nuovo il codice di produzione per passare.

In altre parole, il test potrebbe essere una sorta di test di funzionalità non un semplice test di unità TDD. Questi sono sempre un bene prezioso per la qualità del prodotto.

Personalmente non mi piacciono regole così totalitarie e disumane;


2

In realtà lo stesso problema è emerso in un dojo la scorsa notte.

Ho fatto una rapida ricerca al riguardo. Questo è quello che mi è venuto in mente:

Fondamentalmente non è vietato esplicitamente dalle regole TDD. Forse sono necessari alcuni test aggiuntivi per dimostrare che una funzione funziona correttamente per un input generalizzato. In questo caso la pratica del TDD viene lasciata da parte solo per un po '. Si noti che abbandonare la pratica TDD a breve non significa necessariamente infrangere le regole TDD purché nel frattempo non sia stato aggiunto alcun codice di produzione.

Test aggiuntivi possono essere scritti purché non siano ridondanti. Una buona pratica sarebbe quella di fare test di partizionamento della classe di equivalenza. Ciò significa che vengono testati i casi limite e almeno un caso interno per ogni classe di equivalenza.

Un problema che potrebbe verificarsi con questo approccio, tuttavia, è che se i test passano dall'inizio non si può garantire che non vi siano falsi positivi. Ciò significa che potrebbero esserci dei test che superano perché i test non sono implementati correttamente e non perché il codice di produzione funziona correttamente. Per evitare ciò, il codice di produzione dovrebbe essere leggermente modificato per interrompere il test. Se il test fallisce, è molto probabile che il test sia implementato correttamente e che il codice di produzione possa essere modificato per ripetere il test.

Se vuoi semplicemente praticare il TDD rigoroso potresti non scrivere nessun test aggiuntivo che passi dall'inizio. D'altra parte in un ambiente di sviluppo aziendale si dovrebbe effettivamente abbandonare la pratica TDD se ulteriori test sembrano utili.


0

Un test che passa senza modificare il codice di produzione non è intrinsecamente negativo ed è spesso necessario per descrivere un requisito aggiuntivo o un caso limite. Fintanto che il test "sembra utile", come dici tu, mantienilo.

Il punto in cui ti trovi nei guai è quando scrivi un test già superato in sostituzione della comprensione effettiva dello spazio del problema.

Possiamo immaginare a due estremi: un programmatore che scrive un gran numero di test "per ogni evenienza" si coglie un bug; e un secondo programmatore che analizza attentamente lo spazio del problema prima di scrivere un numero minimo di test. Supponiamo che entrambi stiano cercando di implementare una funzione a valore assoluto.

Il primo programmatore scrive:

assert abs(-88888) == 88888
assert abs(-12345) == 12345
assert abs(-5000) == 5000
assert abs(-32) == 32
assert abs(46) == 46
assert abs(50) == 50
assert abs(5001) == 5001
assert abs(999999) == 999999
...

Il secondo programmatore scrive:

assert abs(-1) == 1
assert abs(0) == 0
assert abs(1) == 1

L'implementazione del primo programmatore potrebbe comportare:

def abs(n):
    if n < 0:
        return -n
    elif n > 0:
        return n

L'implementazione del secondo programmatore potrebbe comportare:

def abs(n):
    if n < 0:
        return -n
    else:
        return n

Tutti i test superano, ma il primo programmatore non ha solo scritto diversi test ridondanti (rallentando inutilmente il loro ciclo di sviluppo), ma non è riuscito a testare un caso limite ( abs(0)).

Se ti ritrovi a scrivere test che superano senza modificare il codice di produzione, chiediti se i tuoi test stanno davvero aggiungendo valore o se devi dedicare più tempo a comprendere lo spazio del problema.


Bene, anche il secondo programmatore è stato chiaramente negligente con i test, perché il suo collega ha ridefinito abs(n) = n*ne superato.
Eiko

@Eiko Hai perfettamente ragione. Scrivendo troppo pochi test può mordere altrettanto male. Il secondo programmatore era eccessivamente avaro non almeno dai test abs(-2). Come in ogni cosa, la chiave è la moderazione.
Thinkterry
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.