Come correggere un errore nel test, dopo aver scritto l'implementazione


21

Qual è la migliore linea d'azione in TDD se, dopo aver implementato correttamente la logica, il test fallisce ancora (perché c'è un errore nel test)?

Ad esempio, supponiamo che tu voglia sviluppare la seguente funzione:

int add(int a, int b) {
    return a + b;
}

Supponiamo di svilupparlo nei seguenti passaggi:

  1. Scrivi test (nessuna funzione ancora):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Risultati nell'errore di compilazione.

  2. Scrivi un'implementazione fittizia della funzione:

    int add(int a, int b) {
        return 5;
    }
    

    Risultato: test1passa.

  3. Aggiungi un altro caso di test:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Risultato: test2fallisce, test1passa ancora.

  4. Scrivi implementazione reale:

    int add(int a, int b) {
        return a + b;
    }
    

    Risultato: test1passa ancora, test2continua a fallire (da allora 11 != 12).

In questo caso particolare: sarebbe meglio:

  1. corretto test2e vedere che ora passa, o
  2. eliminare la nuova porzione di implementazione (es. tornare al passaggio 2 sopra), correggere test2e lasciarla fallire, quindi reintrodurre l'implementazione corretta (passaggio 4 sopra.).

O c'è un altro modo più intelligente?

Mentre capisco che il problema dell'esempio è piuttosto banale, sono interessato a cosa fare nel caso generico, che potrebbe essere più complesso dell'aggiunta di due numeri.

EDIT (in risposta alla risposta di @Thomas Junk):

Il focus di questa domanda è ciò che TDD suggerisce in questo caso, non quale sia "la migliore pratica universale" per ottenere buoni codici o test (che potrebbero essere diversi dal modo TDD).



5
Chiaramente, devi fare TDD sul tuo TDD.
Blrfl,

17
Se qualcuno mi chiederà perché sono scettico nei confronti del TDD, li indicherò a questa domanda. Questo è Kafkaesque.
Traubenfuchs,

@Blrfl questo è ciò che Xibit ci dice »Ho messo il TDD in TDD in modo che tu potessi TDD mentre TDDing«: D
Thomas Junk

3
@Traubenfuchs Ammetto che la domanda sembra sciocca a prima vista e non sono un sostenitore del "fai TDD tutto il tempo", ma credo che ci sia un grande vantaggio nel vedere un test fallito, quindi scrivere un codice che faccia passare il test (che è in realtà questa domanda, dopo tutto).
Vincent Savard,

Risposte:


31

La cosa assolutamente critica è che vedi il test passare e fallire.

Indipendentemente dal fatto che tu elimini il codice per far fallire il test, riscrivi il codice o lo sgattaioli negli appunti solo per incollarlo in un secondo momento, non importa. TDD non ha mai detto che dovevi ridigitare nulla. Vuole sapere che il test passa solo quando dovrebbe passare e fallisce solo quando dovrebbe fallire.

Vedere il test sia passare che fallire è come testare il test. Non fidarti mai di un test che non hai mai visto fare entrambe le cose.


Il refactoring contro The Red Bar ci fornisce passaggi formali per il refactoring di un test di lavoro:

  • Esegui il test
    • Nota la barra verde
    • Rompere il codice in fase di test
  • Esegui il test
    • Nota la barra rossa
    • Rifattorizzare il test
  • Esegui il test
    • Nota la barra rossa
    • Rompere il codice in fase di test
  • Esegui il test
    • Nota la barra verde

Tuttavia, non stiamo eseguendo il refactoring di un test di lavoro. Dobbiamo trasformare un test con errori. Una preoccupazione è il codice che è stato introdotto mentre solo questo test lo ha coperto. Tale codice dovrebbe essere ripristinato e reintrodotto una volta risolto il test.

In caso contrario, e la copertura del codice non è un problema a causa di altri test che coprono il codice, è possibile trasformare il test e introdurlo come test verde.

Anche in questo caso viene eseguito il rollback del codice, ma quanto basta per impedire il test. Se questo non è abbastanza per coprire tutto il codice introdotto mentre è coperto solo dal test con errori, abbiamo bisogno di un rollback del codice più grande e di più test.

Introdurre un test verde

  • Esegui il test
    • Nota la barra verde
    • Rompere il codice in fase di test
  • Esegui il test
    • Nota la barra rossa
    • Rompere il codice in fase di test
  • Esegui il test
    • Nota la barra verde

Rompere il codice può essere commentare il codice o spostarlo altrove solo per incollarlo in un secondo momento. Questo ci mostra l'ambito del codice coperto dal test.

Per queste ultime due corse sei tornato al normale ciclo rosso verde. Stai solo incollando invece di digitare per rompere il codice e far passare il test. Quindi assicurati di incollare solo abbastanza per superare il test.

Lo schema generale qui è vedere il colore del test cambiare come ci aspettiamo. Si noti che ciò crea una situazione in cui si dispone brevemente di un test ecologico non attendibile. Fai attenzione a non essere interrotto e a dimenticare dove ti trovi in ​​questi passaggi.

I miei ringraziamenti a RubberDuck per il link Embracing the Red Bar .


2
Mi piace questa risposta al meglio: è importante vedere il test fallire con un codice errato, quindi eliminerei / commenterei il codice, correggevo i test e li vedevo fallire, ripristinavo il codice (forse introducevo un errore deliberato per mettere i test a il test) e correggi il codice per farlo funzionare. È molto XP per eliminarlo e riscriverlo completamente, ma a volte devi solo essere pragmatico. ;)
GolezTrol

@GolezTrol Penso che la mia risposta dica la stessa cosa, quindi apprezzerei qualsiasi feedback tu abbia avuto sul fatto che non fosse chiaro.
jonrsharpe,

@jonrsharpe Anche la tua risposta è buona e l'ho votata prima ancora di leggere questa. Ma dove sei molto severo nel ripristinare il codice, CandiedOrange suggerisce un approccio più pragmatico che mi piace di più.
GolezTrol,

@GolezTrol Non ho detto come ripristinare il codice; commentalo, taglia e incolla, nascondilo, usa la cronologia del tuo IDE; non importa davvero. La cosa cruciale è il motivo per cui lo fai: in modo da poter verificare che stai ricevendo il giusto fallimento. Ho modificato, spero di chiarire.
jonrsharpe,

10

Qual è l' obiettivo generale che vuoi raggiungere?

  • Fare bei test?

  • Fare la corretta implementazione?

  • Fare TTD religiosamente, giusto ?

  • Nessuno dei precedenti?

Forse pensi troppo alla tua relazione con i test e con i test.

I test non forniscono garanzie sulla correttezza di un'implementazione. Avere tutti i test superati non dice nulla, se il tuo software fa quello che dovrebbe; non fa dichiarazioni essenziali sul tuo software.

Prendendo il tuo esempio:

L'implementazione "corretta" dell'aggiunta sarebbe il codice equivalente a a+b. E fintanto che il tuo codice lo fa , diresti che l'algoritmo è corretto in quello che fa ed è correttamente implementato.

int add(int a, int b) {
    return a + b;
}

A prima vista , saremmo entrambi d'accordo sul fatto che questa è l'implementazione di un'aggiunta.

Ma quello che stiamo facendo davvero non sta dicendo che questo codice è l'implementazione di additionesso si comporta solo in una certa misura come una: pensa al trabocco di numeri interi .

L'overflow di numeri interi si verifica nel codice, ma non nel concetto di addition. Quindi: il tuo codice si comporta in una certa misura come il concetto di addition, ma non lo è addition.

Questo punto di vista piuttosto filosofico ha diverse conseguenze.

E uno è, si potrebbe dire, i test non sono altro che ipotesi sul comportamento previsto del codice. Nel testare il tuo codice, potresti (forse) non assicurarti mai, la tua implementazione è giusta , il meglio che potresti dire è che le tue aspettative sui risultati che il tuo codice fornisce sono state o non sono state soddisfatte; sia, che il tuo codice sia sbagliato, sia, che il tuo test sia sbagliato o che sia, che entrambi siano sbagliati.

Test utili ti aiutano a fissare le tue aspettative su ciò che il codice dovrebbe fare: fintanto che non modifico le mie aspettative e fintanto che il codice modificato mi dà il risultato che mi aspetto, potrei essere sicuro, che le ipotesi che ho fatto i risultati sembrano funzionare.

Questo non aiuta, quando hai fatto le ipotesi sbagliate; ma hey! almeno previene la schizofrenia: aspettarsi risultati diversi quando non ci dovrebbero essere.


tl; dr

Qual è la migliore linea d'azione in TDD se, dopo aver implementato correttamente la logica, il test fallisce ancora (perché c'è un errore nel test)?

I tuoi test sono ipotesi sul comportamento del codice. Se hai buone ragioni per pensare che l'implementazione sia corretta, correggi il test e vedi se tale presupposto è valido.


1
Penso che la domanda sugli obiettivi generali sia piuttosto importante, grazie per averlo sollevato. Per me, il prio più alto è il seguente: 1. implementazione corretta 2. test "belli" (o, preferirei dire, test "utili" / "ben progettati"). Vedo TDD come un possibile strumento per raggiungere questi due obiettivi. Quindi, mentre non voglio necessariamente seguire religiosamente il TDD, nel contesto di questa domanda, sono principalmente interessato alla prospettiva del TDD. Modificherò la domanda per chiarire questo.
Attilio,

Quindi, scriveresti un test che verifica l'overflow e passa quando succede o lo faresti fallire quando succede perché l'algoritmo è addizione e l'overflow produce la risposta sbagliata?
Jerry Jeremiah,

1
@JerryJeremiah Il mio punto è: ciò che i test dovrebbero coprire dipende dal caso d'uso. Per un caso d'uso in cui si sommano un sacco di cifre singole, l'algoritmo è abbastanza buono . Se sai che è molto probabile che sommi "grandi numeri", datatypeè chiaramente la scelta sbagliata. Un test rivelerebbe che: la tua aspettativa sarebbe "funziona per grandi numeri" e in molti casi non è soddisfatta. Quindi la domanda sarebbe come affrontare questi casi. Sono casi angolari? Quando sì, come gestirli? Forse alcune clausole di quard aiutano a prevenire un pasticcio maggiore. La risposta è legata al contesto.
Thomas Junk,

7

Devi sapere che il test fallirà se l'implementazione è sbagliata, il che non equivale a passare se l'implementazione è corretta. Pertanto, è necessario riportare il codice in uno stato in cui si prevede che non abbia esito positivo prima di correggere il test e assicurarsi che non abbia esito positivo per il motivo previsto (ovvero 5 != 12), piuttosto che qualcos'altro che non è stato previsto.


Come possiamo verificare che il test fallisca per il motivo che ci aspettiamo?
Basilevs,

2
@Basilevs: 1. formulare un'ipotesi su quale dovrebbe essere la ragione del fallimento; 2. eseguire il test; e 3. leggi il messaggio di errore risultante e confronta. A volte questo suggerisce anche modi in cui potresti riscrivere il test per darti un errore più significativo (ad esempio, assertTrue(5 == add(2, 3))dà un output meno utile rispetto assertEqual(5, add(2, 3))anche se entrambi stanno testando la stessa cosa).
jonrsharpe,

Non è ancora chiaro come applicare questo principio qui. Ho un'ipotesi: il test restituisce un valore costante, in che modo eseguire nuovamente lo stesso test assicurerebbe che io abbia ragione? Ovviamente per provarlo, ho bisogno di UN ALTRO test. Suggerisco di aggiungere un esempio esplicito per rispondere.
Basilevs,

1
@Basilevs che cosa? La tua ipotesi qui al passaggio 3 sarebbe "il test fallisce perché 5 non è uguale a 12" . L'esecuzione del test ti mostrerà se il test fallisce per quel motivo, nel qual caso procedi o per qualche altro motivo, nel qual caso capisci perché. Forse questo è un problema linguistico, ma non mi è chiaro cosa stai suggerendo.
jonrsharpe,

5

In questo caso particolare, se cambiate il 12 in un 11 e il test ora passa, penso che abbiate fatto un buon lavoro nel testare il test e l'implementazione, quindi non c'è molto bisogno di passare attraverso ulteriori cerchi.

Tuttavia, lo stesso problema può presentarsi in situazioni più complesse, ad esempio quando si verifica un errore nel codice di installazione. In tal caso, dopo aver corretto il test, probabilmente dovresti provare a mutare l'implementazione in modo tale da far fallire quel particolare test, quindi ripristinare la mutazione. Se ripristinare l'implementazione è il modo più semplice per farlo, allora va bene. Nel tuo esempio, potresti mutare a + bin a + ao a * b.

In alternativa, se è possibile mutare leggermente l'asserzione e vedere il fallimento del test, ciò può essere abbastanza efficace nel testare il test.


0

Direi, questo è un caso per il tuo sistema di controllo versione preferito:

  1. Metti in scena la correzione del test, mantenendo le modifiche al codice nella directory di lavoro.
    Commettere con un messaggio corrispondente Fixed test ... to expect correct output.

    Con git, questo potrebbe richiedere l'uso git add -pse test e implementazione sono nello stesso file, altrimenti puoi ovviamente mettere in scena i due file separatamente.

  2. Conferma il codice di implementazione.

  3. Torna indietro nel tempo per testare il commit fatto nel passaggio 1, assicurandoti che il test fallisca effettivamente .

Vedete, in questo modo non fate affidamento sulla vostra abilità di modifica per spostare il codice di implementazione mentre testate il test fallito. Impieghi il tuo VCS per salvare il tuo lavoro e per assicurarti che la cronologia registrata del VCS includa correttamente sia il test fallito che quello passato.

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.