TDD: si tratta solo di test unitari? [duplicare]

37

Capisco bene che il TDD classico riguardi solo i test unitari? Non mi capisco male: conosco la differenza tra TDD e solo test delle unità. Sto chiedendo se è corretto utilizzare il test di integrazione nel flusso di lavoro TDD.

Attualmente lavoro sul progetto in cui TDD è sicuramente solo sui test unitari e c'è almeno un problema serio con esso. La maggior parte dei nostri test unitari sono test comportamentali che spesso diventano falsi negativi (falso rosso) durante il refactoring (solo perché alcune sequenze di chiamate di dipendenza sono cambiate). Il progetto è stato creato in stile TDD per rendere il refactoring semplice e improvvisamente il refactoring è diventato un inferno. Fallimento epico!

La decisione più ovvia ora è di rendere il nostro test non unitario ma integrativo (ma ancora con TDD). Quindi, se in precedenza abbiamo stubato / deriso le dipendenze delle classi, ora non lo faremo (almeno non tutte). Come risultato, la maggior parte dei nostri test diventerà uno stato (invece che comportamentale). E i test di stato diventano falsi negativi molto più raramente (perché testano il risultato ma non il flusso di lavoro dell'esecuzione).

Quindi vorrei sapere quanto sia diffuso un approccio all'utilizzo del TDD con test di integrazione. Va bene? Sarei grato per qualsiasi risorsa su questo argomento. Ho letto l'articolo ma è un po '... strano

Aggiornamento. Qui chiarirò cosa intendo sotto test unitario e test di integrazione. Unit test è un test che blocca / deride tutte le dipendenze di classe. Il test di integrazione ha implementazioni reali delle dipendenze (sebbene possa stubare / deridere alcune dipendenze se necessario).

    
posta SiberianGuy 09.08.2011 - 20:28
fonte

10 risposte

20

Nel TDD, ho intenzione di testare insieme le unità quando ha senso. Per come la vedo io, uso mock / stub per due motivi: il comportamento del sistema diventa troppo complesso per testare efficacemente l'implementazione completa e la piena implementazione può fare cose che non voglio accadere.

Fondamentalmente, più oggetti sono coinvolti in un test, più difficile sarà il sistema da prevedere. Hai previsto il comportamento del sistema per scrivere il test. Alcuni oggetti possono avere un comportamento complesso e può essere difficile indurre casi di bordo particolari in una unità. Quindi può essere veramente utile per deridere quegli oggetti.

In altri casi, gli oggetti hanno effetti collaterali indesiderabili. Supponiamo che tu abbia un CreditCardProcessor. Non vuoi davvero caricare le carte di credito mentre i tuoi test sono in corso. Altri elementi come la grafica di disegno o l'accesso alle risorse web potrebbero essere nella stessa categoria.

Quando un oggetto ha una dipendenza, come decidi se includere l'oggetto reale o una specie di finto / stub?

In primo luogo, se c'è qualche possibilità che il comportamento dell'oggetto cambi durante lo sviluppo, lo spegnerei. Ad esempio, considera una classe di priorità di priorità rispetto a una classe di strategia di prezzo. Quasi certamente una coda di priorità manterrà sempre lo stesso comportamento. Tuttavia, è probabile che la strategia di determinazione dei prezzi cambi molto. Di conseguenza, non vuoi che altri test dipenda dal comportamento all'interno della strategia di prezzo. Se lo fanno, finirai per rompere gli altri test inutilmente. Tuttavia, non è davvero un grosso problema per la coda di priorità perché il comportamento non dovrebbe mai cambiare.

In secondo luogo, in che modo "grasso" è l'interfaccia tra gli oggetti? Se gli oggetti hanno un'interfaccia molto semplice, allora il mocking è facile e lo farò. Se gli oggetti hanno un'interfaccia complessa, il sbeffeggiamento è difficile e meno probabile che valga la pena. In questo caso, contrapponiamo un oggetto di connessione al database e una strategia di prezzo. L'interfaccia delle strategie di prezzo dovrebbe essere ragionevolmente semplice, si spera solo un metodo CalculatePrice (SalesOrderItem). Certo, il codice reale può fare ogni sorta di cose con SalesOrderItem, ma il tuo stub non deve occuparsene. D'altra parte, una connessione al database ha passato istruzioni SQL che gli danno un'interfaccia abbastanza complessa. Prendere in giro il database è davvero difficile perché hai controllato tutte le query che sono state fatte e fornito una risposta corretta. Inoltre, non stai verificando che le query siano valide (solo che corrispondono a quanto ti aspetti), in questi casi il controllo su un database reale ha più senso, in questo modo si verifica effettivamente che le query funzionino e i test passa ancora se riscrivi le query per dare gli stessi risultati ma in un modo diverso.

In terzo luogo, se un oggetto sarà lento lo spengo. Se si dispone di un database, le chiamate ad esso saranno abbastanza lenta che richiede l'uso di uno stub per evitare di dover effettuare una chiamata. Simile per l'accesso Web, ecc.

Ho appena usato i database come esempio di qualcosa che dovresti stub e anche non stub. Ho stub i miei database con un database in memoria sqlite che evita il problema delle prestazioni, ma consente comunque di testare il mio SQL. In realtà sto usando un framework che genera SQL specifico per il mio database per me, quindi è un lavoro.

Nel tuo caso attuale, dichiari:

The majority of our unit tests are behavioural tests which often became false negative (false red) during refactoring (just because some sequence of dependencies calls changed).

Come ho capito, i test falliscono perché prima veniva chiamato foo () e poi bar (). Ora bar () è chiamato then foo (). Se l'ordine di chiamare foo () e bar () non ha importanza, i tuoi test non dovrebbero controllare chi ha chiamato per primo. Il tuo test dovrebbe verificare solo che siano stati entrambi chiamati.

    
risposta data 09.08.2011 - 20:55
fonte
33

Refactoring: modifica della struttura del codice senza modificare il comportamento .

Test unitario - un test che si assicura che un'unità di codice si comporti come previsto.

Se i test dell'unità falliscono perché stai modificando la struttura del tuo codice, i test non testano il comportamento. Stanno testando la struttura. In teoria, se il test fallisce, dovrebbe essere fallito perché il comportamento testato è cambiato, indipendentemente dalla struttura del codice. Quindi se i test non riescono a causa del refactoring, allora i tuoi test devono essere scritti meglio o il tuo refactoring è sbagliato.

Non penso che il problema sia con i test unitari in generale, ma piuttosto sul modo in cui stai scrivendo i tuoi test unitari. Se trovi difficile scrivere i tuoi test unitari in modo che testino solo il comportamento e la struttura non , allora questo è un indicatore che potrebbe essere necessario modificare la progettazione del codice. E questo è tutto ciò che riguarda TDD.

    
risposta data 10.08.2011 - 00:23
fonte
10

I test unitari sono i più facili da scrivere e mantenere quando si scrivono test per funzioni semplici. Mentre ci si sposta verso l'esterno da quello a metodi più complessi che utilizzano strutture di dati complesse, i test unitari diventano sempre più elaborati e costosi. Gli oggetti falsi devono essere impiegati e i test diventano sempre più fragili.

Ad un certo punto, ottieni rendimenti decrescenti.

Tuttavia, non tutto è perduto. Praticare TDD (che in realtà dovrebbe essere chiamato Design, non basato su Test-Driven) spinge a pensare al tuo codice e a scriverlo in modi che lo rendono più testabile.

Risposta breve: la pratica del test delle unità (di cui TDD è un sottoinsieme) può migliorare notevolmente la qualità del codice che scrivi. Ma, come la maggior parte delle cose in informatica, non è un proiettile d'argento.

    
risposta data 09.08.2011 - 20:51
fonte
6

TDD è una mentalità.

Costruire il codice in un modo facilmente verificabile è il modo più semplice per guardare a questo. Il tipo di test dovrebbe essere irrilevante per tutti tranne i casi più estremi.

Unità, integrazione e sistema sono tutti metodi di test e se il tuo sviluppo viene affrontato in modalità test guidato, il tipo di test dovrebbe essere messo nel back burner poiché il tuo codice sarà inevitabilmente passare attraverso vari cicli ed essere testato in modi diversi.

    
risposta data 09.08.2011 - 20:49
fonte
6

Se vuoi la vera definizione di TDD, dovresti leggere il libro di Kent Beck, lo sviluppo guidato dai test . Diversi professionisti del TDD avranno diversi livelli di test che raccomandano. E diverse persone hanno diverse definizioni di test di unità e integrazione *.

Se stai definendo i test unitari come puramente isolati, allora non penso che tu possa dire che TDD riguarda i test unitari. Alcune persone hanno preso il termine "test degli sviluppatori" e non si preoccupano più di quale livello sono presenti. Vi raccomando di preoccuparvi di meno se i vostri test sono unit test e se sono test utili.

Per quanto riguarda i test basati sullo stato rispetto a test comportamentali / di interazione / test di simulazione, preferisco utilizzare test basati sullo stato per articoli che mantengono principalmente lo stato e test basati su mock per elementi che effettuano principalmente connessioni. (In altre parole, utilizzo test basati sullo stato per i miei modelli e sto prendendo in giro i miei controller.)

*: divulgazione completa; Questo è il mio link. L'ho scritto solo perché nessun altro lo aveva ancora fatto, e mi è sembrato prezioso sottolineare la terminologia di terra-miniera qui.

    
risposta data 09.08.2011 - 22:11
fonte
4

Al suo centro, TDD parla di feedback. La dimensione del "Sistema sotto test" riguarda solo l'accuratezza di quel feedback. Più grande è il SUT, più è probabile che si ottengano falsi positivi. Più piccolo è il SUT, maggiore è la probabilità di ottenere quei falsi negativi che descrivi. Sono sicuro che c'è un equilibrio da qualche parte.

Per affrontare questa idea di approccio al TDD con test "più grandi", ti consiglio di leggere Software orientato agli oggetti in crescita, Guidati dai test

Questo potrebbe darti un'idea migliore di quali approcci sono disponibili per costruire uno stile TDD dell'applicazione dall'esterno. Raccolgo che l'approccio descritto è abbastanza diffuso ed è noto come la "London School" di TDD. Ho sicuramente imparato molto sulla creazione di dispositivi di prova comuni e Rolling Fakes per quei confini di integrazione più grandi che non si vogliono superare in tutti i test. L'ampio esempio del libro inizia ciascuna funzionalità con un test di accettazione che genera test più piccoli. La quarta parte contiene molte informazioni sulla costruzione di test, come test su concorrenza, sincronizzazione e chiamate asincrone, su come rendere espressivi i test, su quali test si sta cercando di dirti in termini di design, qualità di un test flessibile, ecc. .

(Caveat: una mezza dozzina di anni fa, TDD classico significava "mockist" basato sullo stato. Ora apparentemente il TDD classico può usare anche i mock ed è diverso dalla "London School". Indipendentemente dalla terminologia, penso che ci sia qualcosa da imparare al di fuori del TDD classico, anche se non si vuole prendere in giro ogni interazione con gli oggetti.)

    
risposta data 09.08.2011 - 23:46
fonte
3

Sono arrivato alla stessa conclusione che hai fatto a un certo punto. Tuttavia, dopo un po 'di test di integrazione (in particolare test web con Selenium), li ho trovati anche dolorosi, per ragioni diverse. Più tardi ho visto questo video intitolato I test di integrazione sono una truffa che mi ha segnalato ancora una volta nel campo di test unitario. Sono felice che tu abbia sollevato il punto di falsi negativi: è un termine che uso anch'io. Il problema è: puoi ottenere molti falsi negativi anche con test di integrazione. Il trucco, quindi, sia che si stiano scrivendo test unitari o test di integrazione, è di minimizzare il costo di mantenimento dei test, ottenendo il massimo beneficio da essi, consentendo di aggiungere funzionalità e codice refactor più rapidamente rilevando gli errori in anticipo. Questa è davvero un'arte e non posso dire di averla incrinata.

I principali problemi con i test di integrazione sono

  1. funzionano lentamente, molto lentamente, il che causerà il non voler eseguirli
  2. perché testano un numero elevato di oggetti, un errore potrebbe derivare da molte cause diverse; inversamente, un problema potrebbe causare il fallimento di un gran numero di test (falsi negativi)
  3. quando un gran numero di oggetti interagisce, il numero di possibili percorsi di codice sfugge. Quindi, non è pratico testarli tutti utilizzando i test di integrazione.
risposta data 10.08.2011 - 05:28
fonte
1

Pensa a cosa mostrano i diversi test:

Un test unitario mostrerà che l'unità di codice fa ciò che hai deciso di fare.

Un test di integrazione mostra che le unità di codice (e forse altre cose come database, file system, sistemi di messaggistica, ecc.) lavorano insieme per fare ciò che dovrebbero fare.

Un test di accettazione da parte degli utenti mostra che l'utente apprezza ciò che tutto quel codice e cose gli permettono di fare.

Non hai veramente finito finché tutte e tre queste cose non sono buone. Non c'è davvero un punto per scrivere un metodo che non faccia quello che dovrebbe, quindi definiscilo prima in un test unitario. Non c'è un motivo per scrivere un gruppo di classi che non funzionano insieme come dovrebbero ---- definirlo prima in un test di integrazione. Infine, è assolutamente necessario sviluppare un sistema che gli utenti non vogliono, quindi ... lo capisci.

    
risposta data 09.08.2011 - 21:20
fonte
1

Funzioni di test TDD. A volte le funzionalità sono unità, a volte sono integrazioni o test di accettazione. Test TDD scale .

    
risposta data 09.08.2011 - 22:59
fonte
1

So I would like to know how widespread is an approach of using TDD with integration tests. Is it okay? Would be grateful for any resources on this topic. I have read this article but it is a bit... strange

Sviluppo guidato dal test di accettazione

Il termine usato per TDD che implica testare il comportamento del sistema in generale piuttosto che una particolare unità di codice.

L'ho usato personalmente con grande efficacia per circa 7 anni ora principalmente nello sviluppo web.

Prima di saltare ti metterei in guardia dall'andare da un estremo (derisione / soppressione di qualsiasi cosa al di fuori dell'unità) a un altro (non gestendo affatto risorse esterne). Dovresti valutare le tue esigenze di sviluppo. Stabilire quali sono i trade-off e scegliere quello che offre il massimo valore. Fai sempre l'analisi, perché a volte le "migliori pratiche" non lo sono.

Ok, sono d'accordo

Non mi preoccuperei che la pratica fosse "okay". Penso che dovresti concentrarti se risolve il tuo problema, o almeno è un netto miglioramento.

    
risposta data 05.03.2013 - 09:25
fonte

Leggi altre domande sui tag