2009-09-15 18 views
5

Nell'articolo Test for Required Behavior, not Incidental Behavior, Kevlin Henney ci ricorda che:Test per comportamento richiesto vs. TDD

"[...] un errore comune in fase di test è quello di cablare test per le specifiche di un'implementazione, dove quelle specifiche sono incidentali e non hanno alcuna relazione con la funzionalità desiderata. "

Tuttavia, quando si utilizza TDD, spesso finisco per scrivere test per comportamento incidentale. Cosa faccio con questi test? Buttarli via sembra sbagliato, ma il consiglio nell'articolo è che questi test possono ridurre l'agilità.

Che ne dici di separarli in una suite di test separata? Sembra un inizio, ma sembra poco pratico in modo intuitivo. Qualcuno lo fa?

+0

Penso che sarebbe d'aiuto se si includesse un esempio. Sicuramente una domanda valida, ma potrebbe essere interpretata in modi diversi. –

risposta

3

Nel mio test di implementazione-dipendente esperienza sono fragili e non riuscirà in maniera massiccia alla prima refactoring. Quello che cerco di fare è concentrarsi sulla creazione di un'interfaccia corretta per una classe durante la scrittura dei test, evitando in modo efficace tali dettagli di implementazione nell'interfaccia. Questo non solo risolve i test fragili, ma promuove anche un design più pulito.

Ciò consente ancora test aggiuntivi che controllano le parti rischiose della mia implementazione selezionata, ma solo come protezione extra per una buona copertura dell'interfaccia "normale" della mia classe.

Per me il grande cambiamento di paradigma è arrivato quando ho iniziato a scrivere test prima ancora di pensare all'implementazione. La mia sorpresa iniziale è stata che è diventato molto più facile generare casi di test "estremi". Poi ho riconosciuto che l'interfaccia migliorata a sua volta ha aiutato a modellare l'implementazione dietro di essa. Il risultato è che il mio codice oggigiorno non fa molto più di quello che l'interfaccia espone, riducendo in modo efficace la necessità della maggior parte dei test di "implementazione".

Durante refactoring degli interni di una classe, tutti i test saranno tenere. Solo nei casi in cui l'interfaccia esposta cambia, potrebbe essere necessario estendere o modificare il set di test.

+0

Ciao Timo. Grazie per la tua risposta. La difficoltà è, con TDD, che i test guidano l'implementazione e quindi sono per definizione dipendenti dall'implementazione. Per quanto ne so, la mia implementazione è coperta al 100% e posso liberamente refactoring dell'implementazione (usando la definizione di refactoring di Feathers - cioè il comportamento non cambia). Il problema che sto affrontando è con la filosofia "non testare per comportamento incidentale": esteticamente ha senso per me, ma praticamente non vedo come posso farlo * senza * perdere la copertura di implementazione fornita dai miei test TDD. –

+0

(ora ho aggiunto il mio test personale alla prima risposta alla mia risposta.) – Timo

1

Il problema che descrivi è molto reale e molto facile da incontrare quando TDDing. In generale, si può dire che non sta testando il comportamento incidentale stesso che è un problema, ma piuttosto se tonnellate di test dipendono da quel comportamento incidentale.

Il principio ASCIUTTO si applica al codice di prova e al codice di produzione. Questo può spesso essere una buona linea guida quando si scrive codice di test. L'obiettivo dovrebbe essere che tutto il comportamento "incidentale" specificato durante il percorso sia isolato, in modo che solo alcuni test fuori dall'intera suite di test li utilizzino. In questo modo, se è necessario ridefinire tale comportamento, è necessario modificare solo alcuni test anziché una grande parte dell'intera suite di test.

Questo è il modo migliore per utilizzare copiosamente interfacce o classi astratte come collaboratori, poiché ciò significa che si ottiene un accoppiamento di classe bassa.

Ecco un esempio di cosa intendo. Supponiamo che tu abbia qualche tipo di implementazione MVC in cui un Controller dovrebbe restituire una Vista. Supponiamo che abbiamo un metodo come questo su un BookController:

public View DisplayBookDetails(int bookId) 

L'implementazione dovrebbe utilizzare un IBookRepository iniettata per ottenere il libro dal database e quindi convertire che per una visualizzazione di quel libro. Potresti scrivere molti test per coprire tutti gli aspetti del metodo DisplayBookDetails, ma potresti anche fare qualcos'altro:

Definire un'interfaccia IBookMapper aggiuntiva e inserirla nel BookController oltre a IBookRepository. L'implementazione del metodo potrebbe quindi essere qualcosa di simile:

public View DisplayBookDetails(int bookId) 
{ 
    return this.mapper.Map(this.repository.GetBook(bookId); 
} 

Ovviamente questo è un esempio di troppo semplicistico, ma il punto è che ora è possibile scrivere una serie di test per l'implementazione IBookMapper reale, il che significa che quando si prova il metodo DisplayBookDetails, si può semplicemente usare uno Stub (meglio generato da un framework mock dinamico) per implementare la mappatura, invece di provare a definire una relazione fragile e complessa tra un oggetto Dominio libro e il modo in cui è mappato.

L'uso di un IBookMaper è sicuramente un dettaglio di implementazione incidentale, ma se si utilizza un meglio ancora un contenitore SUT Factory o auto-beffardo, la definizione di tale incidentale comportamento è isolato il che significa che se in seguito si decide di refactoring l'implementazione, puoi farlo cambiando solo il codice di prova in pochi punti.

1

"Che ne dici di separarli in una suite di test separata?"

Cosa faresti con questa suite separata?

Ecco il caso tipico utilizzo.

  1. Hai scritto alcuni test che testano i dettagli di implementazione che non avrebbero dovuto testare.

  2. Si calcola questi test fuori dalla suite principale in una suite separata.

  3. qualcuno cambia l'implementazione.

  4. La vostra suite di implementazione ora fallisce (come dovrebbe).

Che ora?

  • Risolvi i test di implementazione? Penso di no. Il punto era di non testare un'implementazione perché porta a molto lavoro di manutenzione.

  • Hanno test che possono fallire, ma la corsa complessiva di unittest è ancora considerata buona? Se i test falliscono, ma l'insuccesso non ha importanza, cosa significa? [Leggi questa domanda per un esempio: Non-critical unittest failures Un test ignorato o irrilevante è solo costoso.

Devi scartarli.

Risparmia tempo e irritazione scartandoli ora, non quando falliscono.

0

vi davvero fare TDD il problema non è così grande come può sembrare in una sola volta, perché si sta scrivendo test prima codice. Non dovresti nemmeno pensare a nessuna implementazione possibile prima di scrivere un test.

Tali problemi di test di comportamento incidentale sono molto più comuni quando si scrivono test dopo il codice di implementazione. Quindi, il modo più semplice è solo controllare che l'output della funzione sia OK e faccia ciò che vuoi, quindi scrivere test usando quell'output. In realtà questo è imbroglio, non TDD, e il costo della truffa sono test che si interromperanno se l'implementazione cambierà.

La cosa buona è che tali test si interromperanno ancora più facilmente dei buoni test (buon test significa qui test a seconda della funzione desiderata, non dipende dall'implementazione). Avere test così generici da non rompere mai è molto peggio.

Dove lavoro quello che facciamo è semplicemente fissare tali test quando ci imbattiamo in loro. Come li risolviamo dipende dal tipo di test incidentale effettuato.

  • il tale test più comune è probabilmente il caso in cui i risultati dei test si verifica in un ordine preciso che domina questo ordine è realmente non garantiti. La semplice soluzione è abbastanza semplice: ordina sia i risultati che i risultati attesi. Per strutture più complesse utilizzare alcuni comparatori che ignorano questo tipo di differenze.

  • ogni tanto testiamo la funzione più interna, mentre è più esterna la funzione che esegue la funzione. Questo è male, perché rielaborare la funzione più interna diventa difficile. La soluzione è scrivere un altro test che copra lo stesso range di funzionalità a livello di funzione più esterno, quindi rimuovere il vecchio test e solo allora possiamo rifattorizzare il codice.

  • quando questo test si interrompe e vediamo un modo semplice per renderli indipendenti dall'implementazione, lo facciamo. Tuttavia, se non è facile, potremmo scegliere di risolverli per essere ancora dipendenti dall'implementazione, ma a seconda della nuova implementazione. I test si interromperanno al prossimo cambio di implementazione, ma non è necessariamente un grosso problema. Se è un grosso problema, elimina definitivamente quel test e trova un altro per coprire quella funzione, o cambia il codice per renderlo più facile da testare.

  • un altro caso negativo è quando abbiamo scritto test usando un oggetto Mocked (usato come stub) e poi il cambio di comportamento dell'oggetto deriso (API Change). Questo non va bene perché non infrange il codice quando dovrebbe, perché cambiando il comportamento dell'oggetto deriso non cambia il Mock che lo imita. La correzione qui è di usare l'oggetto reale al posto del mock, se possibile, o riparare il Mock per un nuovo comportamento. In tal caso, sia il comportamento Mock sia il comportamento dell'oggetto reale sono entrambi casuali, ma crediamo che i test che non falliscono quando dovrebbero rappresentare un problema più grande dei test che si interrompono quando non dovrebbero. (Ammettiamo che tali casi possano anche essere risolti a livello di test di integrazione).

+0

Hi kriss. Grazie per la tua risposta. Stiamo facendo TDD nel vero senso della parola, e l'implementazione è completamente guidata da test (seguendo le regole minimaliste dello zio Bob, scrivendo solo un test ecc. Ecc.), Ma ciò significa che i test * per definizione * sono specifici dell'implementazione, giusto ? Penso che potrebbe essere OK in un certo senso, a patto che i test testino componenti sufficientemente piccoli e che i componenti siano separabili. Ma come organizzare i test per il comportamento, vs implementazione? E ci sono sovrapposizioni tra test accidentali (cattivi) e test di implementazione (vale la pena per il refactoring di mantenimento del comportamento)? –