In che modo le persone che fanno TDD gestiscono la perdita di lavoro quando eseguono importanti refactoring


37

Per un po 'ho cercato di imparare a scrivere unit test per il mio codice.

Inizialmente ho iniziato a fare il vero TDD, dove non avrei scritto alcun codice fino a quando non avrei scritto prima un test fallito.

Tuttavia, recentemente ho avuto un problema spinoso da risolvere che ha comportato molto codice. Dopo aver trascorso un paio di settimane a scrivere test e quindi a scrivere codice, sono arrivato alla sfortunata conclusione che il mio intero approccio non avrebbe funzionato e avrei dovuto buttare via due settimane di lavoro e ricominciare da capo.

Questa è una decisione abbastanza sbagliata da prendere quando hai appena scritto il codice, ma quando hai anche scritto diverse centinaia di test unitari diventa ancora più emotivamente difficile buttare via tutto.

Non posso fare a meno di pensare di aver sprecato 3 o 4 giorni di sforzi per scrivere quei test quando avrei potuto mettere insieme il codice per la prova del concetto e poi aver scritto i test dopo essere stato soddisfatto del mio approccio.

In che modo le persone che praticano il TDD gestiscono correttamente tali situazioni? C'è un motivo per piegare le regole in alcuni casi o scrivi sempre prima in modo testato, anche quando quel codice può rivelarsi inutile?


6
La perfezione si ottiene, non quando non c'è altro da aggiungere, ma quando non c'è più niente da togliere. - Antoine de Saint-Exupery
mouviciel,

12
Come è possibile che tutti i tuoi test siano sbagliati? Spiegare come una modifica all'implementazione invalida ogni singolo test scritto.
S. Lott,

6
@ S.Lott: i test non erano sbagliati, non erano più rilevanti. Supponi che stai risolvendo parte di un problema usando i numeri primi, quindi scrivi una classe per generare numeri primi e scrivi dei test per quella classe per assicurarti che funzioni. Ora trovi un'altra soluzione totalmente diversa al tuo problema che non coinvolge in alcun modo i numeri primi. Quella classe e i suoi test ora sono ridondanti. Questa era la mia situazione solo con 10 di classi non solo una.
GazTheDestroyer,

5
@GazTheDestroyer mi sembra che la distinzione tra il codice di test e il codice funzionale sia un errore - fa tutto parte dello stesso processo di sviluppo. È giusto notare che TDD ha un sovraccarico che di solito viene recuperato più in basso nel processo di sviluppo e che sembra che quel sovraccarico non ti abbia guadagnato nulla in questo caso. Ma anche quanto i test hanno informato la tua comprensione dei fallimenti dell'architettura? È anche importante notare che ti è permesso (anzi, incoraggiato ) di potare i tuoi test nel tempo ... anche se questo è probabilmente un po 'estremo (-:
Murph,

10
Sarò semanticamente pedante e sono d'accordo con @ S.Lott qui; quello che hai fatto non è il refactoring se si traduce nel buttare via molte classi e le prove per loro. Questo è il re-architecting . Il refactoring, specialmente in senso TDD, significa che i test erano verdi, hai modificato un po 'di codice interno, rieseguito i test e sono rimasti verdi.
Eric King,

Risposte:


33

Sento che ci sono due problemi qui. Il primo è che non ti sei reso conto in anticipo che il tuo design originale potrebbe non essere l'approccio migliore. Se lo avessi saputo in anticipo, potresti aver scelto di sviluppare uno o due prototipi da buttare via , per esplorare le possibili opzioni di progettazione e per valutare quale sia il modo più promettente da seguire. Nella prototipazione, non è necessario scrivere il codice di qualità della produzione e non è necessario testare l'unità in ogni angolo (o affatto), poiché il tuo unico obiettivo è l'apprendimento, non la lucidatura del codice.

Ora, rendersi conto che hai bisogno di prototipazione ed esperimenti piuttosto che iniziare subito lo sviluppo del codice di produzione, non è sempre facile e nemmeno sempre possibile. Grazie alle conoscenze appena acquisite, potresti essere in grado di riconoscere la necessità di prototipare la prossima volta. O no. Ma almeno ora sai che questa opzione deve essere considerata. E questo di per sé è una conoscenza importante.

L'altro problema è IMHO con la tua percezione. Tutti commettiamo errori, ed è così facile vedere a posteriori cosa avremmo dovuto fare diversamente. Questo è solo il modo in cui impariamo. Annota il tuo investimento in unit test come il prezzo di apprendimento dell'importanza della prototipazione e superalo. Sforzati di non fare lo stesso errore due volte :-)


2
Sapevo che sarebbe stato un problema difficile da risolvere e che il mio codice sarebbe stato un po 'esplorativo, ma sono stato entusiasta dei miei recenti successi nel TDD, quindi ho continuato a scrivere test come avevo fatto dato che è tutto qui la letteratura TDD sottolinea così tanto. Quindi sì, ora so che le regole possono essere infrante (il che è proprio quello di cui mi occupavo davvero) probabilmente lo farò per esperienza.
GazTheDestroyer,

3
"Ho continuato a scrivere test come avevo fatto dato che questo è ciò che tutta la letteratura TDD sottolinea così tanto". Probabilmente dovresti aggiornare la domanda con l'origine della tua idea che tutti i test devono essere scritti prima di qualsiasi codice.
S. Lott,

1
Non ne ho idea e non sono sicuro di come l'hai ottenuto dal commento.
GazTheDestroyer,

1
Stavo per scrivere una risposta, ma invece ho votato a favore della tua. Sì, un milione di volte sì: se non sai ancora come appare la tua architettura, scrivi prima un prototipo usa e getta e non preoccuparti di scrivere test unitari durante la prototipazione.
Robert Harvey,

1
@WarrenP, sicuramente ci sono persone che pensano che TDD sia la sola vera via (qualsiasi cosa può essere trasformata in una religione se ci provi abbastanza ;-). Preferisco essere pragmatico però. Per me TDD è uno strumento nella mia cassetta degli attrezzi, e lo uso solo quando aiuta, piuttosto che ostacolare, la risoluzione dei problemi.
Péter Török,

8

Il punto di TDD è che ti costringe a scrivere piccoli incrementi di codice in piccole funzioni , proprio per evitare questo problema. Se hai trascorso settimane a scrivere codice su un dominio e ogni singolo metodo di utilità che hai scritto diventa inutile quando ripensi all'architettura, i tuoi metodi sono quasi certamente troppo grandi in primo luogo. (Sì, sono consapevole che questo non è esattamente confortante ora ...)


3
I miei metodi non erano affatto grandi, semplicemente divennero irrilevanti data la nuova architettura che non somigliava alla vecchia architettura. In parte perché la nuova architettura era molto più semplice.
GazTheDestroyer,

Va bene, se davvero non c'è nulla di riutilizzabile, puoi solo tagliare le perdite e andare avanti. Ma la promessa di TDD è che ti fa raggiungere gli stessi obiettivi più velocemente, anche se scrivi codice di prova oltre al codice dell'applicazione. Se questo è vero, e credo fermamente che lo sia, allora almeno hai raggiunto il punto in cui hai realizzato come realizzare l'architettura in "un paio di settimane" anziché due volte quella volta.
Kilian Foth,

1
@Kilian, ri "la promessa di TDD è che ti fa raggiungere gli stessi obiettivi più velocemente" - a quali obiettivi ti riferisci qui? È abbastanza ovvio che la scrittura di unit test insieme al codice di produzione stesso ti rende inizialmente più lento , rispetto al semplice sfocatura del codice. Direi che TDD ripagherà a lungo termine, a causa della migliore qualità e dei costi di manutenzione ridotti.
Péter Török,

@ PéterTörök - Ci sono persone che insistono sul fatto che TDD non abbia mai alcun costo perché si paga da solo quando hai scritto il codice. Questo non è certamente il caso per me, ma Killian sembra crederci da solo.
ps

Bene ... se non ci credi, in effetti se non credi che TDD abbia un sostanziale payoff piuttosto che un costo, allora non ha senso farlo, vero? Non solo nella situazione molto specifica descritta da Gaz, ma a tutti . Temo di aver spinto questa discussione completamente fuori tema :(
Kilian Foth,

6

Brooks ha detto "progetta di buttarne uno, lo farai comunque". Mi sembra che tu stia facendo proprio questo. Detto questo, dovresti scrivere i tuoi test unitari per testare unità di codice e non ampie strisce di codice. Questi sono test più funzionali e quindi dovrebbero essere applicati su qualsiasi implementazione interna.

Ad esempio, se voglio scrivere un solutore PDE (equazioni differenziali parziali), scriverei alcuni test cercando di risolvere cose che posso risolvere matematicamente. Questi sono i miei primi test "unit" - leggi: i test funzionali vengono eseguiti come parte di un framework xUnit. Questi non cambieranno a seconda dell'algoritmo che uso per risolvere il PDE. Tutto ciò che mi interessa è il risultato. I test della seconda unità si concentreranno sulle funzioni utilizzate per codificare l'algoritmo e quindi sarebbero specifici dell'algoritmo, ad esempio Runge-Kutta. Se dovessi scoprire che Runge-Kutta non era adatto, avrei comunque quei test di alto livello (compresi quelli che mostravano che Runge-Kutta non era adatto). Pertanto la seconda iterazione avrebbe ancora molti degli stessi test della prima.

Il tuo problema potrebbe riguardare il design e non necessariamente il codice. Ma senza ulteriori dettagli, è difficile da dire.


È solo periferico, ma cos'è PDE?
un CVn

1
@ MichaelKjörling Immagino sia l'equazione differenziale parziale
foraidt

2
Brooks non ha ritirato questa affermazione nella sua seconda edizione?
Simon,

Come vuoi dire che avrai ancora i test che dimostrano che Runge-Kutta non era adatto? Che aspetto hanno questi test? Vuoi dire che hai salvato l'algoritmo Runge-Kutta che hai scritto, prima di scoprire che non era adatto, e che l'esecuzione dei test end-to-end con RK nel mix avrebbe fallito?
moteutsch,

5

È necessario tenere presente che TDD è un processo iterativo. Scrivi un piccolo test (nella maggior parte dei casi poche righe dovrebbero essere sufficienti) ed eseguilo. Il test dovrebbe fallire, ora funziona direttamente sulla tua fonte principale e prova a implementare la funzionalità testata in modo che il test passi. Ora ricominciare da capo.

Non dovresti provare a scrivere tutti i test in una volta sola, perché, come hai notato, questo non funzionerà. Ciò riduce il rischio di perdere tempo a scrivere test che non verranno utilizzati.


1
Non credo di potermi spiegare molto bene. Scrivo test in modo iterativo. È così che ho finito con diverse centinaia di test per il codice che è diventato improvvisamente ridondante.
GazTheDestroyer,

1
Come sopra - penso che dovrebbe essere pensato come "test e codice" piuttosto che "test per codice"
Murph

1
+1: "Non dovresti provare a scrivere tutti i test in una volta sola"
S.Lott

4

Penso che tu l'abbia detto tu stesso: non eri sicuro del tuo approccio prima di iniziare a scrivere tutti i test unitari.

La cosa che ho imparato confrontando i progetti TDD nella vita reale con cui ho lavorato (non molti, in effetti, solo 3 che coprono 2 anni di lavoro) con ciò che avevo imparato teoricamente, è il Test automatico! = Test unitario (senza ovviamente essere reciprocamente esclusivo).

In altre parole, il T in TDD non deve avere una U con esso ... È automatizzato, ma è meno un unit test (come nelle classi e metodi di test) rispetto a un test funzionale automatizzato: è allo stesso livello di granularità funzionale come l'architettura su cui stai attualmente lavorando. Inizi ad alto livello, con pochi test e solo il quadro generale funzionale, e solo alla fine finisci con migliaia di UT e tutte le tue classi ben definite in una bella architettura ...

I test unitari ti danno un grande aiuto quando lavori in gruppo, per evitare modifiche al codice creando cicli infiniti di bug. Ma non ho mai scritto nulla di così preciso quando ho iniziato a lavorare su un progetto, prima di avere almeno un POC di lavoro globale per ogni user story.

Forse è solo il mio modo personale di farlo. Non ho l'esperienza sufficiente per decidere da zero quali schemi o strutture avrà il mio progetto, quindi in effetti non perderò il mio tempo a scrivere centinaia di UT dall'inizio ...

Più in generale, l'idea di rompere tutto e lanciarlo tutto sarà sempre lì. Per quanto "continuo" come possiamo provare a essere con i nostri strumenti e metodi, a volte l'unico modo per combattere l'entropia è ricominciare. Ma l'obiettivo è che quando ciò accadrà, i test automatici e di unità implementati avranno reso il tuo progetto già meno costoso rispetto a se non ci fosse - e lo sarà, se trovi l'equilibrio.


3
ben detto - è TDD, non UTDD
Steven A. Lowe

Risposta eccellente. Nella mia esperienza di TDD è importante che i test scritti si concentrino sui comportamenti funzionali del software e lontano dai test unitari. È più difficile pensare ai comportamenti di cui hai bisogno da una classe, ma porta a interfacce pulite e potenzialmente semplifica l'implementazione risultante (non aggiungi funzionalità che in realtà non ti servono).
JohnTESlade,

4
In che modo le persone che praticano il TDD gestiscono correttamente tali situazioni?
  1. considerando quando prototipare e quando codificare
  2. rendendosi conto che il test unitario non è lo stesso del TDD
  3. da scritti test TDD per verificare una caratteristica / storia, non un'unità funzionale

La conflazione dei test unitari con lo sviluppo guidato dai test è la fonte di molta angoscia e dolore. Quindi rivediamolo ancora una volta:

  • i test unitari riguardano la verifica di ogni singolo modulo e funzione nell'implementazione ; in UT vedrai un'enfasi su cose come metriche di copertura del codice e test che si eseguono molto rapidamente
  • lo sviluppo guidato dai test riguarda la verifica di ogni caratteristica / storia nei requisiti ; in TDD vedrai un'enfasi su cose come scrivere il test prima, assicurarti che il codice scritto non superi l'ambito previsto e refactoring per la qualità

In sintesi: i test unitari sono focalizzati sull'implementazione, TDD ha un focus sui requisiti. Non sono la stessa cosa.


"TDD ha un focus sui requisiti" Non sono assolutamente d'accordo. I test scritti in TDD sono test unitari. Essi fanno verificare ciascuna funzione / metodo. TDD pone l'accento sulla copertura del codice e si preoccupa dei test che vengono eseguiti rapidamente (e sarebbe meglio farlo, dal momento che esegui i test ogni 30 secondi circa). Forse stavi pensando ATDD o BDD?
guillaume31,

1
@ ian31: perfetto esempio di conflazione UT e TDD. Deve essere in disaccordo e riferirti ad alcuni materiali di origine en.wikipedia.org/wiki/Test-driven_development - lo scopo dei test è definire i requisiti del codice . BDD è fantastico. Non ho mai sentito parlare di ATDD, ma a prima vista sembra come applico il ridimensionamento TDD .
Steven A. Lowe,

È possibile utilizzare perfettamente TDD per progettare codice tecnico che non è direttamente correlato a un requisito o a una user story. Troverai innumerevoli esempi di ciò sul web, in libri, conferenze, anche da parte di persone che hanno avviato TDD e lo hanno reso popolare. TDD è una disciplina, una tecnica per scrivere codice, non smetterà di essere TDD a seconda del contesto in cui lo si utilizza.
guillaume31,

Inoltre, dall'articolo di Wikipedia che hai citato: "Le pratiche avanzate di sviluppo guidato dai test possono portare all'ATDD in cui i criteri specificati dal cliente sono automatizzati in test di accettazione, che guidano quindi il tradizionale processo di sviluppo basato su test unitari (UTDD). [ ...] Con ATDD, il team di sviluppo ha ora un obiettivo specifico da soddisfare, i test di accettazione, che li mantiene costantemente concentrati su ciò che il cliente vuole davvero da quella storia dell'utente. " Il che sembra implicare che ATDD sia principalmente incentrato sui requisiti, non su TDD (o UTDD come lo definiscono).
guillaume31,

@ ian31: la domanda del PO sul "lancio di diverse centinaia di test unitari" indicava una confusione di scala. Puoi usare TDD per costruire un capannone se vuoi. : D
Steven A. Lowe,

3

Lo sviluppo guidato dai test ha lo scopo di guidare il tuo sviluppo. I test che scrivi ti aiutano ad affermare la correttezza del codice che stai scrivendo e ad aumentare la velocità di sviluppo dalla prima riga in poi.

Sembra che tu creda che i test siano un peso e che in seguito siano destinati allo sviluppo incrementale. Questa linea di pensiero non è in linea con TDD.

Forse puoi confrontarlo con la tipizzazione statica: sebbene si possa scrivere codice senza informazioni di tipo statico, l'aggiunta di un tipo statico al codice aiuta ad affermare alcune proprietà del codice, liberando la mente e consentendo invece di concentrarsi su una struttura importante, aumentando così la velocità e efficacia.


2

Il problema con un grande refactoring è che puoi e a volte seguirai un percorso che ti porta a capire che hai morso più di quanto puoi masticare. I refactoring giganti sono un errore. Se la progettazione del sistema è difettosa in primo luogo, il refactoring può portarti solo così lontano prima che tu debba prendere una decisione difficile. O lascia il sistema così com'è e aggiralo, oppure pianifica di ridisegnare e apportare alcune importanti modifiche.

C'è comunque un altro modo. Il vero vantaggio del codice di refactoring è quello di rendere le cose più semplici, più facili da leggere e persino più facili da mantenere. Quando ti avvicini a un problema di cui hai incertezza, fai un cambiamento, vai così lontano per vedere dove potrebbe portare al fine di saperne di più sul problema, quindi getta via lo spike e applica un nuovo refactoring basato su ciò che lo spike te l'ho insegnato. Il fatto è che puoi davvero migliorare il tuo codice con certezza solo se i passaggi sono piccoli e i tuoi sforzi di refactoring non superano la tua capacità di scrivere prima i test. La tentazione è scrivere un test, quindi codificare, quindi codificare ancora perché una soluzione può sembrare ovvia, ma presto ti rendi conto che la tua modifica cambierà molti più test, quindi devi fare attenzione a cambiare solo una cosa alla volta.

La risposta quindi è di non rendere mai importante il tuo refactoring. Piccoli passi. Inizia estraendo i metodi, quindi cerca di rimuovere la duplicazione. Quindi passare alle classi di estrazione. Ognuno a piccoli passi, un piccolo cambiamento alla volta. SE stai estraendo il codice, scrivi prima un test. Se stai rimuovendo il codice, rimuovilo ed esegui i test e decidi se i test non funzioneranno più. Un piccolo passo alla volta. Sembra che ci vorrà più tempo, ma in realtà ridurrà notevolmente il tempo di refactoring.

La realtà è tuttavia che ogni picco è apparentemente un potenziale spreco di sforzi. Le modifiche al codice a volte non vanno da nessuna parte e ti ritrovi a ripristinare il codice dal tuo vcs. Questa è solo una realtà di ciò che facciamo di giorno in giorno. Ogni picco che fallisce non viene comunque sprecato, se ti insegna qualcosa. Ogni sforzo di refactoring fallito ti insegnerà che stai provando a fare troppo in fretta, o che il tuo approccio potrebbe essere sbagliato. Anche questo non è una perdita di tempo se impari qualcosa da esso. Più fai queste cose, più impari e più diventerai efficiente. Il mio consiglio è di indossarlo per ora, imparare a fare di più facendo di meno, e accettare che questo è esattamente il modo in cui le cose probabilmente devono essere fino a quando non sarai in grado di identificare quanto lontano fare un picco prima che non ti porti da nessuna parte.


1

Non sono sicuro del motivo per cui il tuo approccio si è rivelato difettoso dopo 3 giorni. A seconda delle incertezze nella tua architettura, potresti considerare di cambiare la tua strategia di test:

  • Se non sei sicuro delle prestazioni, potresti voler iniziare con alcuni test di integrazione che affermano le prestazioni?

  • Quando la complessità dell'API è ciò su cui stai indagando, scrivi alcuni veri e propri test di unità per scoprire quale sarebbe il modo migliore per farlo. Non preoccuparti di implementare nulla, fai semplicemente in modo che le tue classi restituiscano valori con codice fisso o inviano NotImplementedExceptions.


0

Per me i test unitari sono anche un'occasione per mettere l'interfaccia in un uso "reale" (beh, reale quanto i test unitari vanno!).

Se sono costretto a organizzare un test, devo esercitare il mio progetto. Questo aiuta a mantenere le cose sane (se qualcosa è così complesso che scrivere un test è un peso, come sarà usarlo?).

Questo non evita cambiamenti nel design, ma espone la necessità per loro. Sì, una riscrittura completa è un dolore. Per (tentare di) evitarlo, di solito imposto (uno o più) prototipi, possibilmente in Python (con lo sviluppo finale in c ++).

Certo, non hai sempre il tempo per tutte queste prelibatezze. Questi sono proprio i casi in cui avrete bisogno di una GRANDE quantità di tempo per raggiungere i tuoi obiettivi ... e / o per tenere tutto sotto controllo.


0

Benvenuti nel circo degli sviluppatori creativi .


Invece di rispettare tutto il modo 'legale / ragionevole' di programmare all'inizio,
prova l' intuizione , soprattutto se è importante e nuovo per te e se nessun campione in giro ti sembra come vuoi:

- Scrivi con il tuo istinto, da cose che già conosci , non con la tua mente e immaginazione.
- E basta.
- Prendi una lente d'ingrandimento e controlla tutte le parole che scrivi: scrivi "testo" perché "testo" è vicino a String, ma "verbo", "aggettivo" o qualcosa di più preciso è necessario, leggi di nuovo e regola il metodo con un nuovo senso
. .. o hai scritto un pezzo di codice pensando al futuro? rimuoverlo
- Correggere, svolgere altre attività (sport, cultura o altre cose al di fuori degli affari), tornare indietro e rileggere.
- Si adattano tutti bene,
- Corretto, svolgere altre attività, tornare e leggere di nuovo.
- Tutto si adatta bene, passa a TDD
- Ora tutto è corretto, buono
- Prova il benchmark per sottolineare le cose da ottimizzare, fallo.

Cosa appare:
- hai scritto un codice rispettando tutte le regole
- ottieni un'esperienza, un nuovo modo di lavorare,
- qualcosa cambia nella tua mente, non avrai mai paura con una nuova configurazione.

E ora, se vedi un UML simile a quello sopra, sarai in grado di dire
"Boss, comincio da TDD per questo ...."
è un'altra cosa nuova?
"Capo, proverei qualcosa prima di decidere il modo in cui codificherò ..."

Cordiali saluti da PARIS
Claude

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.