Cosa fare quando i test TDD rivelano nuove funzionalità necessarie che necessitano anche di test?


13

Cosa fai quando scrivi un test e arrivi al punto in cui devi superare il test e ti rendi conto che hai bisogno di un ulteriore pezzo di funzionalità che dovrebbe essere separato nella sua stessa funzione? Anche quella nuova funzione deve essere testata, ma il ciclo TDD dice di fare fallire un test, farlo passare poi refactor. Se mi trovo sul gradino in cui sto provando a superare il test, non dovrei partire e iniziare un altro test fallito per testare la nuova funzionalità che devo implementare.

Ad esempio, sto scrivendo una classe di punti che ha una funzione WillCollideWith ( LineSegment ) :

public class Point {
    // Point data and constructor ...

    public bool CollidesWithLine(LineSegment lineSegment) {
        Vector PointEndOfMovement = new Vector(Position.X + Velocity.X,
                                               Position.Y + Velocity.Y);
        LineSegment pointPath = new LineSegment(Position, PointEndOfMovement);
        if (lineSegment.Intersects(pointPath)) return true;
        return false;
    }
}

Stavo scrivendo un test per CollidesWithLine quando mi sono reso conto che avrei bisogno di una funzione LineSegment.Intersects ( LineSegment ) . Ma dovrei semplicemente interrompere ciò che sto facendo nel mio ciclo di test per creare questa nuova funzionalità? Ciò sembra infrangere il principio "Rosso, Verde, Rifattore".

Devo solo scrivere il codice che rileva quella lineaSegments Intersect all'interno della funzione CollidesWithLine e rifattorarla dopo che funziona? Funzionerebbe in questo caso dal momento che posso accedere ai dati da LineSegment , ma che dire nei casi in cui quel tipo di dati è privato?

Risposte:


14

Basta commentare il test e il codice recente (o metterlo in una scorta) in modo da aver di fatto riportato l'orologio all'inizio del ciclo. Quindi iniziare con il LineSegment.Intersects(LineSegment)test / codice / refactor. Al termine, rimuovere il commento dal test / codice precedente (o estrarre dalla scorta) e continuare con il ciclo.


In che modo è diverso quindi semplicemente ignorarlo e tornare più tardi?
Joshua Harris,

1
solo piccoli dettagli: nei rapporti non esiste alcun test aggiuntivo "Ignora me" e se si utilizzano gli stash, il codice è indistinguibile dal caso "pulito".
Javier,

Cos'è una scorta? è come il controllo della versione?
Joshua Harris,

1
alcuni VCS lo implementano come funzionalità (almeno Git e Fossil). Ti consente di rimuovere una modifica ma di salvarla per riapplicare qualche tempo dopo. Non è difficile farlo manualmente, basta salvare un diff e ripristinare l'ultimo stato. Successivamente riapplica il diff e continua.
Javier,

6

Sul ciclo TDD:

Nella fase "fai passare il test", dovresti scrivere l'implementazione più semplice che farà passare il test . Per superare il test, hai deciso di creare un nuovo collaboratore per gestire la logica mancante perché era forse troppo lavoro da inserire nella tua classe di punti per eseguire il test. Ecco dove sta il problema. Suppongo che il test che stavi cercando di superare sia stato un passo troppo grande . Quindi penso che il problema risieda nel tuo test stesso, dovresti eliminare / commentare quel test e capire un test più semplice che ti permetterà di fare un piccolo passo senza introdurre la parte LineSegment.Intersects (LineSegment). Uno che ha quel test che passa, puoi quindi refactoringil tuo codice (qui applicherai il principio SRP) spostando questa nuova logica in un metodo LineSegment.Intersects (LineSegment). I test continueranno comunque perché non avrai modificato alcun comportamento ma hai spostato un po 'di codice.

Sulla tua attuale soluzione di design

Ma per me, hai un problema di progettazione più profondo qui è che stai violando il principio di responsabilità singola . Il ruolo di un punto è .... essere un punto, tutto qui. Non c'è intelligenza nell'essere un punto, è solo valore xey. I punti sono tipi di valore . Questo è lo stesso per i segmenti, i segmenti sono tipi di valore composti da due punti. Possono contenere un po 'di "intelligenza", ad esempio per calcolare la loro lunghezza in base alla posizione dei punti. Ma questo è tutto.

Ora decidere se un punto e un segmento si stanno scontrando, è tutta una responsabilità a sé stante. Ed è certamente troppo lavoro per un punto o segmento da gestire da solo. Non può appartenere alla classe Point, perché altrimenti i punti conosceranno i segmenti. E non può appartenere ai segmenti perché i segmenti hanno già la responsabilità di occuparsi dei punti all'interno del segmento e forse anche di calcolare la lunghezza del segmento stesso.

Quindi questa responsabilità dovrebbe essere di proprietà di un'altra classe come ad esempio un "PointSegmentCollisionDetector" che avrebbe un metodo come:

bool AreInCollision (Punto p, Segmento s)

E questo è qualcosa che testeresti separatamente da punti e segmenti.

La cosa bella di quel design è che ora potresti avere una diversa implementazione del tuo rilevatore di collisioni. Quindi sarebbe facile, ad esempio, confrontare il tuo motore di gioco (suppongo che stai scrivendo un gioco: p) cambiando il tuo metodo di rilevamento delle collisioni in fase di esecuzione. O per fare alcuni controlli visivi / esperimenti in fase di esecuzione tra diverse strategie di rilevamento delle collisioni.

Al momento, inserendo questa logica nella tua classe di punti, stai bloccando le cose e spingendo troppe responsabilità sulla classe di punti.

Spero abbia senso


Hai ragione sul fatto che stavo cercando di provare un cambiamento troppo grande e penso che tu abbia ragione a separarlo in una classe di collisione, ma questo mi fa fare una domanda completamente nuova con cui potresti essere in grado di aiutarmi: Dovrei utilizzare un'interfaccia quando i metodi sono solo simili? .
Joshua Harris,

2

La cosa più semplice da fare in modo TDD sarebbe estrarre un'interfaccia per LineSegment e modificare il parametro del metodo per prendere l'interfaccia. Quindi potresti deridere il segmento della linea di input e codificare / testare il metodo Intersect in modo indipendente.


1
So che è il metodo TDD che sento di più, ma un ILineSegment non ha senso. Una cosa è interfacciare una risorsa esterna o qualcosa che potrebbe presentarsi in molte forme, ma non riesco a vedere un motivo per cui avrei mai collegato una qualsiasi funzionalità a qualsiasi altra parte che un segmento di linea.
Joshua Harris,

0

Con jUnit4 è possibile utilizzare l' @Ignoreannotazione per i test che si desidera rinviare.

Aggiungi l'annotazione a ciascun metodo che desideri rimandare e continua a scrivere test per la funzionalità richiesta. Tornare indietro per refactificare i casi di test più vecchi in seguito.

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.