Sono confuso su quale sia il modo corretto di lavorare con TDD

8

Sto cercando di capire qual è l'idea alla base di TDD e come dovrebbe funzionare una squadra. Ho il seguente test case con NUnit + Moq (basta scrivere a memoria, non è sicuro che l'esempio compili ma dovrebbe essere esplicativo):

[Test]
public void WhenUserLogsCorrectlyIsRedirectedToLoginCorrectView() {
    Mock<IUserDatabaseRepository> repoMock = new Mock<IUserDatabaseRepository>();
    repoMock.Setup(m => m.GetUser(It.IsAny())).Returns(new User { Name = "Peter" });        

    Mock<ILoginHelper> loginHelperMock = new Mock<ILoginHelper>();
    loginHelperMock.Setup(m => m.Login(It.IsAny(), It.IsAny())).Returns(true);
    Mock<IViewModelFactory> factoryMock = new Mock<IViewModelFactory>();
    factoryMock.Setup(m => m.CreateViewModel()).Returns(new LoginViewModel());

    AccountController controller = new AccountController(repoMock.Object, loginHelperMock.Object, factoryMock.Object)

    var result = controller.Index(username : "Peter", password: "whatever");

    Assert.AreEqual(result.Model.Username, "Peter");
}

AccountController ha 3 dipendenze che faccio finta che, quando orchestrato all'interno del controller, mi permetta di verificare se un login fosse corretto o meno.

Ciò che mi incuriosisce è che ... se in TDD in teoria dovessi scrivere prima la tua suite di test e costruire il tuo codice da esso, come dovrei sapere in anticipo che per poter eseguire la mia operazione è necessario utilizzare queste tre dipendenze e che l'operazione chiamerà determinate operazioni? È come se avessi bisogno di conoscere le parti interne del Subject Under Test prima ancora di implementarlo per simulare le dipendenze e isolare la classe, creando una sorta di test di scrittura - codice di scrittura - modifica il test se necessario ciclo.

Naturalmente, senza alcuna conoscenza delle parti interne del mio codice e solo nell'esprimere il test, potrei esprimerlo come se potesse semplicemente aver bisogno di ILoginHelper e "magicamente" supporre prima di scrivere il codice che restituirà l'utente su un login riuscito (e alla fine si rende conto che il framework sottostante non funziona in questo modo, ad esempio restituendo solo un ID invece dell'oggetto completo).

Sto comprendendo il TDD in modo errato? Qual è una tipica pratica TDD su un caso complesso?

Grazie

    
posta David Jiménez Martínez 24.11.2014 - 18:45
fonte

6 risposte

19

if in TDD in theory you have to write first your test suit and build your code up from it

Qui è il tuo fraintendimento. TDD è non sulla scrittura di una completa suite di test - questo è un falso mito. TDD significa lavorare in piccoli cicli,

  • scrivendo un test alla volta
  • implementa solo il codice necessario per rendere il test "verde"
  • refactor (il codice e i test)

Quindi la creazione di una suite di test non viene eseguita in un unico passaggio, e non "prima che il codice sia scritto", è intrecciata con l'implementazione del codice in gioco.

Applicato al tuo esempio: dovresti provare a iniziare con un semplice test per un controller senza dipendenze (qualcosa come un prototipo). Quindi implementare il controller e refactoring. Successivamente, si aggiunge un nuovo test che si aspetta che il controller esegua un po 'di più, o si refactoring / estendere il test esistente. Quindi modifichi il controller finché il nuovo test diventa "verde". In questo modo inizi con una semplice combinazione di test & soggetto sotto test, e finire con un test complesso & soggetto sotto test.

Quando si va su questa rotta, ad un certo punto nel tempo si scopriranno quali dati aggiuntivi sono necessari come input affinché il controller possa fare il suo lavoro. Questo può effettivamente accadere in un momento in cui si tenta di implementare un metodo di controllo, e non quando si progetta il prossimo test. Questo è il punto in cui ci si ferma a implementare il metodo per un breve periodo e si inizia a introdurre prima le dipendenze mancanti (magari con un refactoring del costruttore del controllore). Questo porta direttamente a un refactoring dei test esistenti: in TDD, in genere prima cambia i test chiamando il costruttore e successivamente aggiungi i nuovi attributi del costruttore. Ed è qui che la codifica e la scrittura dei test diventano completamente impigliati.

    
risposta data 25.11.2014 - 07:58
fonte
13

What stumbles my mind is that... if in TDD in theory you have to write first your test suit and build your code up from it, how am I supposed to know beforehand that in order to perform my operation I'll need to use those three dependencies and that the operation will call certain operations? It's like I need to know the innards of the Subject Under Test before even implementing it in order to Mock the dependencies and isolate the class, creating some kind of write test - write code - modify test if necessary cycle.

Sembra sbagliato, giusto? E dovrebbe - non perché i tuoi test per il controller siano errati o "cattivi" in alcun modo, ma perché vuoi testare il controller prima di avere qualcosa di "controllato". :)

Il mio punto è: TDD ti sembrerà più naturale una volta che inizi a farlo a livello di "regole aziendali" e "logica dell'applicazione reale", che è anche il punto in cui è più utile. I controllori di solito gestiscono solo la delega ad altri componenti, quindi è naturale che, per verificare se la delega è eseguita correttamente, è necessario sapere a quale oggetto delegherà. L'unico problema è quando provi a farlo prima di avere una vera logica implementata. Il mio suggerimento è di provare ad implementare LoginHelper, ad esempio, facendo TDD in un modo più "orientato al comportamento". Si sentirà più naturale e probabilmente ne vedrete di più.

Quindi a una risposta più generica: TDD è una pratica con la quale produciamo test prima di scrivere il codice che ci serve, ma non specifica quale tipo di test. I controllori sono di solito degli integratori di componenti, quindi scrivi test unitari che di solito richiedono un sacco di derisione. Quando si scrive la logica dell'applicazione (regole di business, come un ordine, la convalida dell'autenticazione dell'utente, ecc.), Si scrivono test di comportamento, che di solito sono test basati sullo stato (input dato vs. output desiderato). Questa differenza viene spesso definita come Mockism vs. Statism dalla comunità TDD. Faccio parte del (piccolo) gruppo che insiste sul fatto che entrambi i modi sono corretti, è solo che offrono diversi compromessi, quindi sono utili per diversi scenari come quelli sopra descritti.

    
risposta data 24.11.2014 - 22:21
fonte
4

Sebbene TDD sia un metodo test-first, non richiede di dedicare molto tempo alla scrittura del codice di test prima di scrivere qualsiasi codice di produzione.

Per questo esempio, l'idea di TDD descritta nel libro di Kent Beck su TDD ( 1 ) è inizia con qualcosa di veramente semplice, come forse

AccountController controller = new AccountController()

var result = controller.Index(username : "Peter", password: "whatever");

Assert.AreEqual(result.Model.Username, "Peter");

In un primo momento, non sai tutto quello di cui hai bisogno che tu faccia il lavoro. Sai solo che avrai bisogno di un controller con un metodo Index che ti dia un modello con un nome utente. Non sai come lo farà ancora. Hai appena fissato un obiettivo per te.

Quindi puoi farlo funzionare con qualsiasi mezzo disponibile, possibilmente solo codificando il risultato corretto all'inizio. Quindi, in successivi refactoring (e anche aggiungendo ulteriori test) si aggiunge una maggiore sofisticazione un passo alla volta. TDD ti consentiamo di fare il passo più breve di cui hai bisogno per andare avanti, ma ti lascia anche libero di fare il passo più ampio di quanto le tue abilità e conoscenze lo consentano. Prendendo un breve ciclo tra codice di prova e codice di produzione, ottieni un feedback su ogni piccolo passo che fai e conosci con quasi immediatezza se quello che hai appena fatto funziona e se ha rotto qualsiasi altro che funzionasse prima.

Robert Martin in ( 2 ) sostiene anche un tempo di ciclo molto breve tra scrivere codice di prova e scrivere produzione codice.

    
risposta data 24.11.2014 - 19:53
fonte
3

Potresti aver bisogno di tutta questa complessità per un test unitario concettualmente semplice, ma quasi certamente non scriveresti il test come questo in primo luogo.

Prima di tutto, il complesso set-up delle prime sei righe dovrebbe essere scomposto in un codice di dispositivo autonomo e riutilizzabile. I principi della programmazione gestibile si applicano al codice di prova proprio come il codice aziendale; se si utilizza la stessa apparecchiatura per due o più test, è consigliabile eseguirne il refactoring in un metodo separato in modo da avere solo una riga distrazione nel test o nel codice di impostazione della classe in modo da non averne.

Ma la cosa più importante: scrivere prima un test non garantisce che possa rimanere invariato per sempre . Se non conosci i collaboratori di una chiamata al metodo, quasi sicuramente non sarai in grado di indovinarli correttamente al primo tentativo. Non c'è nulla di sbagliato nel rifattorizzare il codice di prova insieme al codice aziendale se le API pubbliche cambiano. È vero che l'obiettivo di TDD è di scrivere in primo luogo l'API corretta e utilizzabile, ma questo non è quasi mai raggiunto al 100%. I requisiti always cambiano dopo il fatto, e troppo spesso questo richiede assolutamente collaboratori che non esistevano quando hai scritto la prima iterazione di una storia. In tal caso non c'è nulla da fare, ma mordere il proiettile e modificare i test esistenti insieme alla vostra applicazione; e quelle sono le occasioni in cui la maggior parte del codice di installazione che citi entra nella tua suite di test.

    
risposta data 24.11.2014 - 19:05
fonte
2

It's like I need to know the innards of the Subject Under Test before even implementing it in order to Mock the dependencies and isolate the class, creating some kind of write test - write code - modify test if necessary cycle.

Sì, in una certa misura lo fai. Quindi non penso che tu stia fraintendendo il funzionamento di TDD.

Il problema è che, come altri hanno già detto, all'inizio sembra molto strano, quasi sbagliato farlo in questo modo. A mio parere, questo in realtà mostra ciò che ritengo sia il più grande vantaggio di TDD: devi comprendere correttamente i requisiti prima di scrivere il codice.

Come programmatori, ci piace scrivere codice. Quindi, ciò che ci sembra "giusto" e "naturale" a noi per soddisfare le esigenze e rimanere bloccato nel più rapidamente possibile. I problemi di progettazione diventano poi gradualmente evidenti man mano che si accumula e si verifica il codice base. Quindi ti rifatti e aggiusti e le cose gradualmente migliorano e si muovono verso il tuo obiettivo.

Anche se divertente, questo non è un modo particolarmente efficiente di fare le cose. Molto meglio avere una conoscenza corretta di cosa dovrebbe fare un modulo software per primo, scaricare i test e poi scrivere il codice. È meno refactoring, meno manutenzione del test e ti costringe a scegliere un'architettura migliore del blocco.

Non faccio un sacco di TDD, e penso che il mantra della "copertura del codice al 100%" sia inutile. Soprattutto in casi come il tuo. Tuttavia, l'adozione di TDD ha ancora molto valore perché è un vantaggio nell'assicurarsi che le cose siano ben progettate e ben mantenute per tutto il codice.

Quindi, in sintesi, il fatto che stai trovando questo bizzarro è probabilmente un buon segno del fatto che sei sulla strada giusta.

    
risposta data 25.11.2014 - 11:49
fonte
0

La derisione dei dati è solo la pratica dell'uso di dati fittizi. I framework Moq rendono "facile" la creazione di dati fittizi.

ARRANGE | ACT | ASSERT

In genere TDD riguarda la creazione dei test e quindi la convalida di tali test "passano". Inizialmente, il primo test fallirà poiché il codice per convalidare quel test non è stato ancora creato .. Credo che questo sia in realtà un certo tipo di test; test "rosso / verde", che sono sicuro sia la fonte dei metodi "Test Driven" oggi.

In generale, i test convalidano le piccole porzioni di logica che fanno funzionare il codice immagine più grande. Potresti iniziare al più piccolo livello di funzione, e poi arrivare alle funzioni più complicate.

sì, a volte il setup o "beffeggiatura" sarà alquanto intenso, motivo per cui l'uso di un framework moq è una buona idea, tuttavia, se ci si concentra sulla logica del core business, allora i test porteranno a una garanzia vantaggiosa che funzioni come previsto e inteso.

Personalmente, non collaudo i miei controllori perché tutto ciò che il controller sta usando è stato testato per funzionare, e in generale, non abbiamo bisogno di testare il framework.

    
risposta data 25.11.2014 - 20:28
fonte

Leggi altre domande sui tag