Difficoltà con TDD e refactoring (o - Perché è più doloroso di quanto dovrebbe essere?)


20

Volevo insegnarmi come usare l'approccio TDD e avevo un progetto su cui volevo lavorare da un po '. Non era un grande progetto, quindi ho pensato che sarebbe stato un buon candidato per TDD. Tuttavia, sento che qualcosa è andato storto. Lasciami fare un esempio:

Ad alto livello il mio progetto è un componente aggiuntivo per Microsoft OneNote che mi permetterà di tracciare e gestire i progetti più facilmente. Ora, volevo anche mantenere la logica di business per questo il più possibile disaccoppiata da OneNote nel caso in cui avessi deciso di creare il mio spazio di archiviazione personalizzato e il back-end un giorno.

Per prima cosa ho iniziato con un test di accettazione di base in parole semplici per delineare ciò che volevo fare il mio primo film. Sembra qualcosa del genere (scioccandolo per brevità):

  1. I clic dell'utente creano il progetto
  2. Tipi di utenti nel titolo del progetto
  3. Verificare che il progetto sia stato creato correttamente

Saltando le cose dell'interfaccia utente e un po 'di pianificazione intermedia, arrivo al mio primo test unitario:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

Fin qui tutto bene. Rosso, verde, refattore, ecc. Bene, ora ha davvero bisogno di salvare roba. Ritagliando alcuni passaggi qui finisco con questo.

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

Mi sento ancora bene a questo punto. Non ho ancora un archivio dati concreto, ma ho creato l'interfaccia come mi aspettavo.

Salto alcuni passaggi qui perché questo post sta diventando abbastanza lungo, ma ho seguito processi simili e alla fine arrivo a questo test per il mio archivio di dati:

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

Questo è andato bene fino a quando ho provato a implementarlo:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

E c'è il problema proprio dove si trova il "...". Adesso mi rendo conto che CreatePage richiede un ID sezione. Non me ne sono reso conto quando stavo pensando a livello di controller perché mi occupavo solo di testare i bit rilevanti per il controller. Tuttavia, fin qui mi rendo conto che devo chiedere all'utente una posizione in cui archiviare il progetto. Ora devo aggiungere un ID di posizione all'archivio dati, quindi aggiungerne uno al progetto, quindi aggiungerne uno al controller e aggiungerlo a TUTTI i test già scritti per tutte queste cose. È diventato noioso molto rapidamente e non posso fare a meno di pensare che l'avrei preso più rapidamente se avessi abbozzato il progetto in anticipo piuttosto che lasciarlo progettato durante il processo TDD.

Qualcuno può spiegarmi se ho fatto qualcosa di sbagliato in questo processo? Esiste comunque questo tipo di refactoring che può essere evitato? O è comune? Se è comune, ci sono modi per renderlo più indolore?

Ringrazia tutti!


Riceveresti alcuni commenti molto approfonditi se pubblichi questo argomento in questo forum di discussione: groups.google.com/forum/#!forum/… che è specificamente per gli argomenti TDD.
Chuck Krutsinger,

1
Se devi aggiungere qualcosa a tutti i tuoi test, sembra che i tuoi test siano scritti male. Dovresti riformattare i tuoi test e considerare l'utilizzo di un dispositivo ragionevole.
Dave Hillier,

Risposte:


19

Mentre TDD è (giustamente) pubblicizzato come un modo per progettare e far crescere il tuo software, è comunque una buona idea pensare in anticipo al design e all'architettura. IMO, "abbozzare il progetto in anticipo" è un gioco equo. Spesso questo sarà a un livello superiore rispetto alle decisioni di progettazione che verranno guidate attraverso TDD, tuttavia.

È anche vero che quando le cose cambiano, di solito dovrai aggiornare i test. Non c'è modo di eliminarlo completamente, ma ci sono alcune cose che puoi fare per rendere i tuoi test meno fragili e minimizzare il dolore.

  1. Per quanto possibile, mantieni i dettagli di implementazione fuori dai tuoi test. Ciò significa solo testare con metodi pubblici e, ove possibile, favorire la verifica basata sullo stato rispetto all'interazione . In altre parole, se testate il risultato di qualcosa piuttosto che i passaggi per arrivarci, i test dovrebbero essere meno fragili.

  2. Riduci al minimo la duplicazione nel tuo codice di test, proprio come faresti nel codice di produzione. Questo post è un buon riferimento. Nel tuo esempio, sembra doloroso aggiungere la IDproprietà al costruttore perché lo hai invocato direttamente in diversi test. Invece, prova a estrarre la creazione dell'oggetto in un metodo o a inizializzarlo una volta per ogni test in un metodo di inizializzazione del test.


Ho letto i meriti dello stato rispetto a quello dell'interazione e lo capisco il più delle volte. Tuttavia, non vedo come sia possibile in ogni caso senza esporre ESPLICITAMENTE le proprietà per il test. Prendi il mio esempio sopra. Non sono sicuro di come verificare che l'archivio dati sia stato effettivamente chiamato senza utilizzare un'asserzione per "MustHaveBeenCalled". Per quanto riguarda il punto 2, hai assolutamente ragione. Ho finito per farlo dopo tutte le modifiche, ma volevo solo assicurarmi che il mio approccio fosse generalmente coerente con le pratiche TDD accettate. Grazie!
Landon,

@Landon Ci sono casi in cui il test di interazione è più appropriato. Ad esempio, verifica che sia stata effettuata una chiamata a un database o un servizio web. Fondamentalmente, ogni volta che è necessario isolare il test, soprattutto da un servizio esterno.
jhewlett,

@Landon Sono un "classicista convinto", quindi non ho molta esperienza con i test basati sull'interazione ... Ma non è necessario fare un'affermazione per "MustHaveBeenCalled". Se si sta verificando un inserimento, è possibile utilizzare una query per vedere se è stato inserito. PS: utilizzo stub a causa di considerazioni sulle prestazioni quando collaudo tutto tranne il livello del database.
Hbas

@jhewlett Questa è anche la conclusione a cui sono arrivato. Grazie!
Landon,

@Hbas Non c'è database da interrogare. Sono d'accordo che sarebbe il modo più semplice di procedere se ne avessi uno, ma lo sto aggiungendo a un notebook OneNote. Il meglio che posso fare invece è aggiungere un metodo Get alla mia classe helper di interoperabilità per provare a tirare la pagina. Potrei scrivere il test per farlo, ma mi sentivo come se stessi testando due cose contemporaneamente: l'ho salvato? e la mia classe helper recupera correttamente le pagine? Tuttavia, immagino che a un certo punto i tuoi test potrebbero dover fare affidamento su altro codice testato altrove. Grazie!
Landon,

10

... Non posso fare a meno di pensare che sarei riuscito a prenderlo più velocemente se avessi abbozzato il progetto in anticipo anziché lasciarlo progettato durante i processi TDD ...

Forse sì forse no

Da un lato, TDD ha funzionato perfettamente, offrendoti test automatici durante la creazione di funzionalità e interrompendo immediatamente quando hai dovuto cambiare l'interfaccia.

D'altra parte, forse se avessi iniziato con la funzione di alto livello (SaveProject) invece di una funzione di livello inferiore (CreateProject), avresti notato prima i parametri mancanti.

Poi di nuovo, forse non avresti. È un esperimento irripetibile.

Ma se stai cercando una lezione per la prossima volta: inizia dall'alto. E pensa al design quanto vuoi prima.


0

https://frontendmasters.com/courses/angularjs-and-code-testability/ Dalle 2:22:00 circa alla fine (circa 1 ora). Mi dispiace che il video non sia gratuito, ma non ne ho trovato uno gratuito che lo spieghi così bene.

Una delle migliori presentazioni di scrittura di codice testabile è in questa lezione. È una classe AngularJS, ma la parte di testing riguarda tutto il codice java, principalmente perché ciò di cui sta parlando non ha nulla a che fare con la lingua, e tutto ha a che fare con la scrittura di un buon codice testabile in primo luogo.

La magia sta nello scrivere codice testabile, piuttosto che scrivere test di codice. Non si tratta di scrivere codice che finge di essere un utente.

Trascorre anche un po 'di tempo a scrivere le specifiche sotto forma di asserzioni di prova.

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.