Come mantieni il funzionamento dei tuoi test unitari durante il refactoring?

26

In un'altra domanda, è stato rivelato che uno dei problemi con TDD è mantenere la suite di test in sincronia con il codebase durante e dopo il refactoring.

Ora, sono un grande fan del refactoring. Non ho intenzione di rinunciare a fare TDD. Ma ho anche sperimentato i problemi dei test scritti in modo tale che piccoli refactoring portano a molti fallimenti del test.

Come evitare di rompere i test durante il refactoring?

  • scrivi i test "meglio"? Se sì, cosa dovresti cercare?
  • Eviti certi tipi di refactoring?
  • Esistono strumenti di refactoring dei test?

Modifica: Ho scritto una nuova domanda che ha chiesto cosa intendevo chiedere (ma ha mantenuto questo come una variante interessante).

    
posta Alex Feinman 21.09.2010 - 14:52
fonte

7 risposte

34

Quello che stai cercando di fare non è davvero il refactoring. Con il refactoring, per definizione, non cambierai cosa fa il tuo software, modifichi come lo fa.

Inizia con tutti i test verdi (tutti i passaggi), quindi apporta modifiche "sotto il cofano" (ad esempio, sposta un metodo da una classe derivata alla base, estrai un metodo o incapsula un Composito con un Builder , ecc.). I tuoi test dovrebbero ancora passare.

Quello che stai descrivendo sembra non essere un refactoring, ma una riprogettazione, che aumenta anche la funzionalità del tuo software in prova. TDD e refactoring (come ho cercato di definirlo qui) non sono in conflitto. Puoi ancora refactoring (verde-verde) e applicare TDD (rosso-verde) per sviluppare la funzionalità "delta".

    
risposta data 21.09.2010 - 15:28
fonte
20

Uno dei vantaggi di avere i test unitari è così che puoi tranquillamente refactoring.

Se il refactoring non modifica l'interfaccia pubblica, lasci i test unitari come sono e assicurati che dopo il refactoring passino tutti.

Se il refactoring cambia l'interfaccia pubblica, i test dovrebbero essere prima riscritti. Refactor fino al passaggio dei nuovi test.

Non eviterei mai alcun refactoring perché rompe i test. Scrivere test di unità può essere un dolore in un culo, ma vale la pena il dolore a lungo termine.

    
risposta data 21.09.2010 - 15:16
fonte
6

Contrariamente alle altre risposte, è importante notare che alcuni modi per testare possono diventare fragili quando il sistema in prova (SUT) viene refactored, if il test è whitebox.

Se utilizzo un framework di simulazione che verifica l' ordine dei metodi chiamati sui mock (quando l'ordine è irrilevante perché le chiamate sono prive di effetti collaterali); quindi se il mio codice è più pulito con quelle chiamate di metodo in un ordine diverso e io refactoring, allora il mio test si interromperà. In generale, i mock possono introdurre fragilità nei test.

Se sto controllando lo stato interno del mio SUT esponendo i suoi membri privati o protetti (potremmo usare "amico" in visual basic o scalare il livello di accesso "interno" e usare "internalsvisibleto" in c #; in molti OO lingue, tra cui c # a " specifico-test-sottoclasse " potrebbe essere usato) quindi improvvisamente lo stato interno della classe sarà importante - potresti refactoring della classe come una scatola nera, ma i test della scatola bianca falliranno. Supponiamo che un singolo campo venga riutilizzato per significare cose diverse (non buone pratiche!) Quando il SUT cambia stato - se lo dividiamo in due campi, potremmo aver bisogno di riscrivere test non funzionanti.

Le sottoclassi specifiche del test possono anche essere utilizzate per testare metodi protetti - il che può significare che un refactoring dal punto di vista del codice di produzione è un cambiamento di rottura dal punto di vista del codice di test. Spostare alcune linee dentro o fuori un metodo protetto potrebbe non avere effetti collaterali di produzione, ma interrompere un test.

Se utilizzo " ganci di prova " o qualsiasi altro codice di compilazione specifico o condizionale, può essere difficile assicurati che i test non si interrompano a causa delle fragili dipendenze dalla logica interna.

Quindi, per evitare che i test si uniscano ai dettagli interni intimi del SUT, può aiutare a:

  • Usa gli stub anziché i mock, se possibile. Per maggiori informazioni vedi il blog di Fabio Periera sui test tautologici e il mio blog sui test tautologici .
  • Se utilizzi i mock, evita di verificare l'ordine dei metodi chiamati, a meno che non sia importante.
  • Cerca di evitare di verificare lo stato interno del tuo SUT - usa la sua API esterna, se possibile.
  • Cerca di evitare la logica specifica del test nel codice di produzione
  • Cerca di evitare l'utilizzo di sottoclassi specifiche del test.

Tutti i punti sopra riportati sono esempi di accoppiamenti white-box usati nei test. Quindi, per evitare completamente il refactoring dei test di rottura, usa il test black-box del SUT.

Dichiarazione di non responsabilità: allo scopo di discutere del refactoring qui, sto usando la parola un po 'più in generale per includere il cambiamento di implementazione interna senza effetti esterni visibili. Alcuni puristi potrebbero non essere d'accordo e fare riferimento esclusivamente al libro Refactoring di Martin Fowler e Kent Beck, che descrive le operazioni di refactoring atomico.

In pratica, tendiamo a prendere passi non violenti leggermente più grandi delle operazioni atomiche qui descritte, e in particolare le modifiche che lasciano il codice di produzione che si comporta in modo identico dall'esterno potrebbero non lasciare passare i test. Ma penso che sia giusto includere "un algoritmo sostitutivo per un altro algoritmo che ha un comportamento identico" come un refactoring, e penso che Fowler sia d'accordo. Lo stesso Martin Fowler afferma che il refactoring potrebbe rompere i test:

When you write a mockist test, you are testing the outbound calls of the SUT to ensure it talks properly to its suppliers. A classic test only cares about the final state - not how that state was derived. Mockist tests are thus more coupled to the implementation of a method. Changing the nature of calls to collaborators usually cause a mockist test to break.

[...]

Coupling to the implementation also interferes with refactoring, since implementation changes are much more likely to break tests than with classic testing.

Fowler - Mocks aren't stubs

    
risposta data 06.12.2014 - 14:00
fonte
5

Se i tuoi test si interrompono quando esegui il refactoring, non sei, per definizione, refactoring, che "modifica la struttura del tuo programma senza modificare il comportamento del tuo programma".

A volte è necessario modificare il comportamento dei test. Forse hai bisogno di unire due metodi insieme (ad esempio, bind () e listen () su una classe socket TCP in ascolto), quindi hai altre parti del tuo codice che provano e non riescono a utilizzare l'API ora modificata. Ma questo non è refactoring!

    
risposta data 21.09.2010 - 15:29
fonte
3

Penso che il problema con questa domanda sia che persone diverse stanno prendendo la parola "refactoring" in modo diverso. Penso che sia meglio definire con cura alcune cose che probabilmente intendi:

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

Come già notato da un'altra persona, se stai mantenendo l'API uguale e tutti i tuoi test di regressione funzionano sull'API pubblica, non dovresti avere problemi. Il refactoring non dovrebbe causare alcun problema. Qualsiasi test fallito EITHER significa che il tuo vecchio codice aveva un bug e il tuo test è sbagliato, o il tuo nuovo codice ha un bug.

Ma è abbastanza ovvio. Quindi, PROBABILMENTE intendi per refactoring, che stai modificando l'API.

Quindi fammi rispondere come avvicinarti a questo!

  • Crea innanzitutto una NUOVA API, che fa ciò che vuoi che sia il tuo nuovo comportamento dell'API. Se accade che questa nuova API abbia lo stesso nome di un'API OLDER, quindi aggiungo il nome _NEW al nuovo nome dell'API.

    int DoSomethingInterestingAPI ();

diventa:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

OK - in questa fase - tutti i tuoi test di regressione passano - utilizzando il nome DoSomethingInterestingAPI ().

NEXT, passa attraverso il tuo codice e cambia tutte le chiamate a DoSomethingInterestingAPI () nella variante appropriata di DoSomethingInterestingAPI_NEW (). Ciò include l'aggiornamento / riscrittura di qualsiasi parte dei test di regressione che devono essere modificati per utilizzare la nuova API.

NEXT, contrassegna DoSomethingInterestingAPI_OLD () come [[deprecato ()]]. Mantieni l'API obsoleta per tutto il tempo che desideri (finché non hai aggiornato in modo sicuro tutto il codice che potrebbe dipendere da esso).

Con questo approccio, eventuali errori nei test di regressione sono semplicemente bug in quel test di regressione o identificano bug nel codice, esattamente come si vorrebbe. Questo processo graduale di revisione di un'API creando esplicitamente le versioni _NEW e _OLD dell'API consente di avere bit del codice nuovo e vecchio che coesistono da un po '.

    
risposta data 17.07.2018 - 03:34
fonte
0

I tuoi test sono troppo strettamente collegati all'implementazione e non al requisito.

considera di scrivere i tuoi test con commenti come questo:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

in questo modo non puoi rifattorizzare il significato dei test.

    
risposta data 24.01.2014 - 10:53
fonte
-1

Suppongo che i tuoi test unitari siano di una granularità che definirei "stupidi" :) cioè, testano le minuzie assolute di ogni classe e funzione. Allontanati dagli strumenti del generatore di codice e scrivi test che si applicano a una superficie più grande, puoi refactoring gli interni quanto vuoi, sapendo che le interfacce per le tue applicazioni non sono cambiate ei tuoi test funzionano ancora.

Se desideri avere test unitari che testano ogni singolo metodo, allora aspettati di doverli rifattorizzare allo stesso tempo.

    
risposta data 15.08.2011 - 02:13
fonte

Leggi altre domande sui tag