In che modo i test unitari facilitano la progettazione?


43

Il nostro collega promuove la scrittura di test di unità come in realtà ci aiuta a perfezionare il nostro design e le cose del refactor, ma non vedo come. Se sto caricando un file CSV e lo analizzo, in che modo unit test (convalida dei valori nei campi) mi aiuterà a verificare il mio progetto? Ha menzionato l'accoppiamento, la modularità ecc., Ma per me non ha molto senso - ma non ho molti retroscena teorici.

Non è la stessa della domanda che hai contrassegnato come duplicato, sarei interessato ad esempi concreti su come questo aiuta, non solo alla teoria che dice "aiuta". Mi piace la risposta qui sotto e il commento, ma vorrei saperne di più.


7
Possibile duplicato di TDD porta al buon design?
moscerino del

3
La risposta qui sotto è davvero tutto ciò che devi sapere. Seduto accanto a quelle persone che scrivono per tutto il giorno fabbriche di fabbrica iniettate con dipendenza da radice aggregata, c'è un ragazzo che scrive tranquillamente un semplice codice contro test unitari che funzionano correttamente, è facile da verificare ed è già documentato.
Robert Harvey,

4
@gnat facendo unit test non implica automaticamente TDD, è una domanda diversa
Joppe

11
"unit test (convalida dei valori nei campi)" - sembra che si stiano combinando unit test con la convalida dell'input.
jonrsharpe,

1
@jonrsharpe Dato che si tratta di codice che analizza un file CSV, potrebbe parlare di un test di unità reale che verifica che una determinata stringa CSV fornisca l'output previsto.
JollyJoker,

Risposte:


3

Non solo i test unitari facilitano la progettazione, ma questo è uno dei loro principali vantaggi.

Scrivere test-first elimina la modularità e la struttura pulita del codice.

Quando scrivi il tuo codice test-first, scoprirai che qualsiasi "condizione" di una determinata unità di codice viene naturalmente espulsa alle dipendenze (di solito tramite mock o stub) quando le assumi nel tuo codice.

"Data condizione x, aspettarsi comportamento y" diventerà spesso uno stub da fornire x(che è uno scenario in cui il test deve verificare il comportamento del componente corrente) e ydiventerà un mock, una chiamata a cui verrà verificato all'indirizzo la fine del test (a meno che non sia un "dovrebbe restituire y", nel qual caso il test verificherà semplicemente il valore restituito in modo esplicito).

Quindi, una volta che questa unità si comporta come specificato, si passa alla scrittura delle dipendenze (per xe y) scoperte.

Ciò rende la scrittura di codice pulito e modulare un processo molto semplice e naturale, dove altrimenti è spesso facile confondere le responsabilità e accoppiare i comportamenti senza rendersene conto.

Scrivere test in seguito ti dirà quando il tuo codice è scarsamente strutturato.

Quando scrivere test per un pezzo di codice diventa difficile perché ci sono troppe cose da stubare o deridere, o perché le cose sono troppo strettamente accoppiate insieme, sai che hai miglioramenti da apportare al tuo codice.

Quando "cambiare i test" diventa un peso perché ci sono così tanti comportamenti in una singola unità, sai che hai miglioramenti da apportare nel tuo codice (o semplicemente nel tuo approccio alla scrittura dei test - ma questo non è solitamente il caso nella mia esperienza) .

Quando i tuoi scenari diventano troppo complicati ("if xand yand zthen ...") perché devi estrarre di più, sai che hai dei miglioramenti da apportare al tuo codice.

Quando finisci con gli stessi test in due dispositivi diversi a causa della duplicazione e della ridondanza, sai di avere miglioramenti da apportare al tuo codice.

Ecco un eccellente discorso di Michael Feathers che dimostra la stretta relazione tra testabilità e design nel codice (originariamente pubblicato da displayName nei commenti). Il discorso affronta anche alcune lamentele comuni e idee sbagliate sul buon design e testabilità in generale.


@SSECommunity: con solo 2 voti a oggi questa risposta è molto facile da trascurare. Consiglio vivamente il discorso di Michael Feathers che è stato collegato in questa risposta.
displayName

103

La cosa grandiosa dei test unitari è che ti permettono di usare il tuo codice come gli altri programmatori useranno il tuo codice.

Se il tuo codice è scomodo per il test unitario, probabilmente sarà scomodo da usare. Se non puoi iniettare dipendenze senza saltare attraverso i cerchi, probabilmente il tuo codice sarà poco flessibile da usare. E se hai bisogno di spendere molto tempo a impostare i dati o a capire in quale ordine eseguire le cose, il tuo codice in prova probabilmente ha troppi accoppiamenti e sarà un problema con cui lavorare.


7
Bella risposta. Mi piace sempre pensare ai miei test come al primo client del codice; se è doloroso scrivere i test, sarà doloroso scrivere codice che consuma l'API o qualunque cosa io stia sviluppando.
Stephen Byrne,

41
Nella mia esperienza, la maggior parte dei test unitari non "usa il tuo codice come gli altri programmatori useranno il tuo codice". Usano il tuo codice poiché i test unitari useranno il codice. È vero, riveleranno molti gravi difetti. Ma un'API progettata per i test unitari potrebbe non essere l'API più adatta per l'uso generale. I test unitari scritti in modo semplice richiedono spesso che il codice sottostante esponga troppi interni. Ancora una volta, in base alla mia esperienza, sarei interessato a sapere come hai gestito questo. (Vedi la mia risposta di seguito)
user949300,

7
@ user949300 - Prima non credo molto nei test. La mia risposta si basa prima sull'idea del codice (e certamente del design). Le API non dovrebbero essere progettate per i test unitari, dovrebbero essere progettate per i tuoi clienti. I test unitari aiutano ad approssimare il cliente, ma sono uno strumento. Sono lì per servirti, non viceversa. E certamente non ti impediranno di creare codice scadente.
Telastyn,

3
Il mio più grande problema con i test unitari nella mia esperienza è che scrivere quelli buoni è difficile quanto scrivere un buon codice in primo luogo. Se non riesci a distinguere un buon codice da un codice errato, scrivere test unit non migliorerà il tuo codice. Quando si scrive il test unitario, è necessario essere in grado di distinguere tra un utilizzo regolare, piacevole e uno "imbarazzante" o difficile. Potrebbero farti usare un po 'il tuo codice, ma non ti costringono a riconoscere che quello che stai facendo è male.
jpmc26,

2
@ user949300 - il classico esempio che avevo in mente qui è un repository che necessita di un connString. Supponete di esporlo come proprietà scrivibile pubblica e di impostarlo dopo aver new () un repository. L'idea è che dopo la quinta o sesta volta hai scritto un test che dimentica di fare quel passo - e quindi si blocca - sarai "naturalmente" incline a forzare connString ad essere un invariante di classe - passato nel costruttore - rendendo così il tuo API migliore e rendendo più probabile la scrittura del codice di produzione che evita questa trappola. Non è una garanzia ma aiuta, imo.
Stephen Byrne,

31

Mi ci è voluto un po 'di tempo per rendermene conto, ma il vero vantaggio (modifica: secondo me, il tuo chilometraggio può variare) nello sviluppo guidato dai test ( usando i test unitari) è che devi progettare l'API in anticipo !

Un tipico approccio allo sviluppo è innanzitutto capire come risolvere un determinato problema, e con quella conoscenza e la progettazione dell'implementazione iniziale in qualche modo invocare la tua soluzione. Questo può dare alcuni risultati piuttosto interessanti.

Quando fai TDD devi prima scrivere il codice che utilizzerà la tua soluzione. Parametri di input e output previsto in modo da poter essere sicuri che sia corretto. Ciò a sua volta richiede di capire cosa è effettivamente necessario per farlo, in modo da poter creare test significativi. Quindi e solo allora implementerai la soluzione. È anche la mia esperienza che quando sai esattamente cosa dovrebbe raggiungere il tuo codice, diventa più chiaro.

Quindi, dopo che i test unitari di implementazione ti aiutano a garantire che il refactoring non interrompa la funzionalità e fornisci la documentazione su come utilizzare il tuo codice (che sai che è giusto al superamento del test!). Ma questi sono secondari: il più grande vantaggio è la mentalità nella creazione del codice in primo luogo.


Questo è certamente un vantaggio, ma non penso che sia il "vero" vantaggio - il vero vantaggio deriva dal fatto che scrivere test per il tuo codice spinge naturalmente le "condizioni" alle dipendenze e annulla l'eccessiva iniezione di dipendenze (promuovendo ulteriormente l'astrazione ) prima che inizi.
Formica P

Il problema è che scrivi un intero set di test in anticipo che corrispondono a quell'API, quindi non funziona esattamente come necessario e devi riscrivere il tuo codice e tutti i test. Per le API rivolte al pubblico è probabile che non cambino e questo approccio va bene. Tuttavia, le API per il codice utilizzato solo internamente cambiano molto man mano che scopri come implementare una funzione che necessita di molte API semi private che lavorano insieme
Juan Mendes,

@AntP Sì, fa parte del design dell'API.
Thorbjørn Ravn Andersen,

@JuanMendes Questo non è raro e quei test dovranno essere cambiati, proprio come qualsiasi altro codice quando si cambiano i requisiti. Un buon IDE ti aiuterà a riformattare le classi come parte del lavoro svolto automaticamente quando cambi le firme del metodo, ecc.
Thorbjørn Ravn Andersen,

@JuanMendes se stai scrivendo buoni test e piccole unità, l'impatto dell'effetto che stai descrivendo è piccolo a nessuno in pratica.
Ant P

6

Concordo al 100% che i test unitari ci aiutano "ci aiutano a perfezionare il nostro design e le cose del refactor".

Ho due idee sul fatto che ti aiutino a fare il progetto iniziale . Sì, rivelano evidenti difetti e ti costringono a pensare a "come posso rendere testabile il codice"? Ciò dovrebbe comportare un minor numero di effetti collaterali, una configurazione e impostazioni più semplici, ecc.

Tuttavia, nella mia esperienza, test di unità eccessivamente semplicistici, scritti prima che tu capisca veramente quale dovrebbe essere il progetto (è vero, è un'esagerazione del TDD hard-core, ma troppo spesso i programmatori scrivono un test prima di pensare molto) spesso portano ad un'anemia modelli di dominio che espongono troppi interni.

La mia esperienza con TDD è stata diversi anni fa, quindi sono interessato a sapere quali nuove tecniche potrebbero aiutare a scrivere test che non pregiudicano troppo il design sottostante. Grazie.


Un lungo numero di parametri del metodo sono un odore di codice e un difetto di progettazione.
Sufian,

5

Il test unitario ti consente di vedere come funzionano le interfacce tra le funzioni e spesso ti fornisce informazioni su come migliorare sia la progettazione locale sia la progettazione generale. Inoltre, se si sviluppano i test unitari durante lo sviluppo del codice, si dispone di una suite di test di regressione già pronta. Non importa se stai sviluppando un'interfaccia utente o una libreria di back-end.

Una volta sviluppato il programma (con test unitari), poiché i bug vengono scoperti, è possibile aggiungere test per confermare che i bug sono stati corretti.

Uso TDD per alcuni dei miei progetti. Mi sono impegnata moltissimo nella creazione di esempi tratti da libri di testo o da documenti considerati corretti e testando il codice che sto sviluppando usando questi esempi. Qualsiasi incomprensione che ho riguardo ai metodi diventa molto evidente.

Tendo ad essere un po 'più flessibile di alcuni dei miei colleghi, poiché non mi interessa se il codice viene scritto per primo o il test viene scritto per primo.


Questa è un'ottima risposta per me. Ti dispiacerebbe fare alcuni esempi, ad esempio uno per ogni caso (quando si ottiene una visione approfondita del design, ecc.).
Utente 039402

5

Quando si desidera eseguire il test unitario del parser che rileva correttamente la delimitazione del valore, è possibile passare una riga da un file CSV. Per rendere il tuo test diretto e breve potresti voler testarlo attraverso un metodo che accetta una riga.

Questo ti farà automaticamente separare la lettura delle righe dalla lettura dei singoli valori.

Ad un altro livello potresti non voler inserire tutti i tipi di file CSV fisici nel tuo progetto di test ma fare qualcosa di più leggibile, semplicemente dichiarando una grande stringa CSV all'interno del tuo test per migliorare la leggibilità e l'intento del test. Questo ti porterà a disaccoppiare il tuo parser da qualsiasi I / O che faresti altrove.

Solo un esempio di base, inizia a praticarlo, a un certo punto sentirai la magia (ho).


4

In parole povere, la scrittura di unit test aiuta a mettere in evidenza i difetti del codice.

Questa spettacolare guida alla scrittura di codice testabile , scritta da Jonathan Wolter, Russ Ruffer e Miško Hevery, contiene numerosi esempi di come i difetti del codice, che possono inibire i test, impediscono anche un facile riutilizzo e flessibilità dello stesso codice. Pertanto, se il tuo codice è testabile, è più facile da usare. La maggior parte della "morale" sono suggerimenti ridicolmente semplici che migliorano notevolmente la progettazione del codice ( Dependency Injection FTW).

Ad esempio: è molto difficile verificare se il metodo computeStuff funziona correttamente quando la cache inizia a sfrattare roba. Questo perché devi aggiungere manualmente schifezze alla cache fino a quando il "bigCache" è quasi pieno.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Tuttavia, quando utilizziamo l'iniezione di dipendenza è molto più semplice verificare se il metodo computeStuff funziona correttamente quando la cache inizia a sfrattare roba. Tutto ciò che facciamo è creare un test in cui chiamiamo new HereIUseDI(buildSmallCache()); Notice, abbiamo un controllo più sfumato dell'oggetto e paga immediatamente i dividendi.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Benefici simili possono essere ottenuti quando il nostro codice richiede dati che sono normalmente conservati in un database ... basta passare ESATTAMENTE i dati necessari.


2
Onestamente, non sono sicuro di come intendi l'esempio. In che modo il metodo computeStuff si collega alla cache?
Giovanni V,

1
@ user970696 - Sì, sto insinuando che "computeStuff ()" utilizza la cache. La domanda è "ComputeStuff () funziona sempre correttamente (che dipende dallo stato della cache)" Di conseguenza, è difficile confermare che computeStuff () fa quello che vuoi PER TUTTI GLI STATI CACHE POSSIBILI se non puoi impostare direttamente / compilare la cache perché è stata codificata la riga "cacheOfExpensiveComputation = buildBigCache ();" (invece di passare direttamente nella cache tramite il costruttore)
Ivan

0

A seconda di cosa si intenda per "Test unitari", non credo che i test unitari di basso livello facilitino un buon design tanto quanto i test di integrazione di livello leggermente superiore - test che testano che un gruppo di attori (classi, funzioni, qualunque cosa) in il codice si combina correttamente per produrre tutta una serie di comportamenti desiderabili concordati tra il team di sviluppo e il proprietario del prodotto.

Se riesci a scrivere test a quei livelli, ti spinge verso la creazione di un codice carino, logico, simile a un'API che non richiede molte dipendenze pazze - il desiderio di avere una semplice configurazione di test ti spingerà naturalmente a non avere un sacco di dipendenze pazze o codice strettamente accoppiato.

Tuttavia, non commettere errori: i test unitari possono portare a una cattiva progettazione, nonché a una buona progettazione. Ho visto gli sviluppatori prendere un po 'di codice che ha già un bel design logico e una singola preoccupazione, e smontarlo e introdurre più interfacce esclusivamente a scopo di test e, di conseguenza, rendere il codice meno leggibile e più difficile da cambiare , oltre a possibilmente anche avere più bug se lo sviluppatore ha deciso che avere molti test unitari di basso livello significa che non devono avere test di livello superiore. Un esempio preferito in particolare è un bug che ho risolto in cui c'era un sacco di codice "testabile" molto scomposto relativo all'ottenimento di informazioni dentro e fuori dagli appunti. Tutto suddiviso e disaccoppiato a livelli molto piccoli di dettaglio, con molte interfacce, molte derisioni nei test e altre cose divertenti. Solo un problema: non esisteva alcun codice che interagisse effettivamente con il meccanismo degli appunti del sistema operativo,

I test unitari possono sicuramente guidare il tuo design, ma non ti guidano automagicamente a un buon design. Devi avere idee su quale sia il buon design che vada oltre il semplice "questo codice è testato, quindi è testabile, quindi è buono".

Naturalmente se sei una di quelle persone per le quali "unit test" significa "eventuali test automatici che non sono guidati attraverso l'interfaccia utente", alcuni di questi avvisi potrebbero non essere così rilevanti - come ho detto, penso che quelli più alti i test di integrazione di livello sono spesso i più utili quando si tratta di guidare il proprio progetto.


-2

I test unitari possono aiutare con il refactoring quando il nuovo codice supera tutti i vecchi test.

Supponiamo che tu abbia implementato un bolle d'aria perché eri di fretta e non preoccupato per le prestazioni, ma ora vuoi una risposta rapida perché i dati si allungano. Se tutti i test passano, le cose sembrano buone.

Naturalmente i test devono essere completi per far funzionare questo. Nel mio esempio, i tuoi test potrebbero non coprire la stabilità perché ciò non riguardava il bolle.


1
Questo è vero ma è più un vantaggio di manutenibilità che un impatto diretto sulla qualità di progettazione del codice.
Formica P

@AntP, l'OP ha chiesto di refactoring e unit test.
om

1
La domanda menzionava il refactoring ma la vera domanda era su come i test unitari potessero migliorare / verificare la progettazione del codice, non facilitare il processo di refactoring stesso.
Ant P

-3

Ho riscontrato che i test unitari sono estremamente utili per facilitare la manutenzione a lungo termine di un progetto. Quando torno a un progetto dopo mesi e non ricordo molti dettagli, l'esecuzione dei test mi impedisce di rompere le cose.


6
Questo è certamente un aspetto importante dei test, ma in realtà non risponde alla domanda (che non è il motivo per cui i test sono buoni, ma come influenzano il design).
Hulk,
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.