Come dovrebbero essere scritti esattamente i test unitari senza deridere ampiamente?


80

A quanto ho capito, il punto dei test unitari è testare le unità di codice in modo isolato . Ciò significa che:

  1. Non dovrebbero interrompere alcuna modifica di codice non correlata altrove nella base di codice.
  2. Solo un test unitario dovrebbe superare un bug nell'unità testata, al contrario dei test di integrazione (che possono rompersi in cumuli).

Tutto ciò implica che ogni dipendenza esterna di un'unità testata dovrebbe essere derisa. E intendo tutte le dipendenze esterne , non solo i "livelli esterni" come reti, filesystem, database, ecc.

Ciò porta a una conclusione logica, che praticamente ogni test unitario deve prendere in giro . D'altra parte, una rapida ricerca di Google sul deridere rivela tonnellate di articoli che affermano che "il deridere è un odore di codice" e dovrebbero, per la maggior parte (anche se non completamente), essere evitati.

Ora, alla domanda (e).

  1. Come devono essere scritti correttamente i test unitari?
  2. Dove sta esattamente la linea tra loro e i test di integrazione?

Aggiornamento 1

Si prega di considerare il seguente pseudo codice:

class Person {
    constructor(calculator) {}

    calculate(a, b) {
        const sum = this.calculator.add(a, b);

        // do some other stuff with the `sum`
    }
}

Un test che testa il Person.calculatemetodo senza deridere la Calculatordipendenza (dato che Calculatorè una classe leggera che non accede al "mondo esterno") può essere considerato un test unitario?


10
Parte di questa è solo l'esperienza progettuale che arriverà col tempo. Imparerai come strutturare i tuoi componenti in modo che non abbiano molte dipendenze difficili da deridere. Ciò significa che la testabilità deve essere un obiettivo di progettazione secondario di qualsiasi software. Questo obiettivo è molto più facile da soddisfare se i test sono scritti prima o insieme al codice, ad esempio utilizzando TDD e / o BDD.
amon,

33
Metti i test eseguiti in modo rapido e affidabile in una cartella. Metti i test che sono lenti e potenzialmente fragili in un altro. Esegui i test nella prima cartella il più spesso possibile (letteralmente ogni volta che ti fermi a digitare e la compilazione del codice è l'ideale, ma non tutti gli ambienti di sviluppo lo supportano). Esegui i test più lenti meno spesso (ad esempio quando fai una pausa caffè). Non preoccuparti per i nomi di unità e integrazione. Chiamali veloci e lenti se vuoi. Non importa
David Arno,

6
"che praticamente ogni test unitario deve prendere in giro" Sì, quindi? "una rapida ricerca di Google sul deridere rivela tonnellate di articoli che affermano che" deridere è un odore di codice "" sono sbagliati .
Michael,

13
@Michael Semplicemente affermando "sì, quindi" e dichiarando la visione opposta sbagliata non è un ottimo modo per affrontare un argomento controverso come questo. Forse scrivi una risposta ed elabora il motivo per cui pensi che il beffardo dovrebbe essere ovunque, e forse perché pensi che "tonnellate di articoli" siano intrinsecamente sbagliate?
AJFaraday,

6
Dato che non hai dato una citazione per "deridere è un odore di codice", posso solo supporre che stai fraintendendo ciò che leggi. Deridere non è un odore di codice. La necessità di usare la riflessione o altri shenanigani per iniettare le tue beffe è un odore di codice. La difficoltà di deridere è inversamente proporzionale alla qualità del tuo design API. Se riesci a scrivere test unitari semplici e semplici che passano solo i mock ai costruttori, lo stai facendo bene.
TKK,

Risposte:


59

il punto dei test unitari è testare le unità di codice in modo isolato.

Martin Fowler su Unit Test

Spesso si parla di unit test nello sviluppo di software, ed è un termine che ho familiarità con i miei programmi di scrittura per tutto il tempo. Come la maggior parte della terminologia di sviluppo del software, tuttavia, è molto mal definita e vedo che spesso può verificarsi confusione quando le persone pensano che sia più strettamente definita di quanto non sia in realtà.

Ciò che Kent Beck ha scritto in Test Driven Development, ad esempio

Li chiamo "test unitari", ma non corrispondono molto bene alla definizione accettata di test unitari

Qualsiasi affermazione relativa a "il punto di unit test" è fortemente dipendente dalla definizione di "unit test" considerata.

Se la tua prospettiva è che il tuo programma sia composto da molte piccole unità che dipendono l'una dall'altra, e se ti limiti a uno stile che testa ciascuna unità in modo isolato, allora molti doppi di test sono una conclusione inevitabile.

I consigli contrastanti che vedi provengono da persone che operano sotto una diversa serie di ipotesi.

Ad esempio, se stai scrivendo test per supportare gli sviluppatori durante il processo di refactoring e dividere un'unità in due è un refactoring che dovrebbe essere supportato, allora qualcosa deve dare. Forse questo tipo di test ha bisogno di un nome diverso? O forse abbiamo bisogno di una diversa comprensione di "unità".

Potresti voler confrontare:

Un test che testa il metodo Person.calculate senza deridere la dipendenza della calcolatrice (dato che la calcolatrice è una classe leggera che non accede al "mondo esterno") può essere considerato un test unitario?

Penso che sia la domanda sbagliata da porre; è di nuovo un argomento sulle etichette , quando credo che ciò che ci interessa davvero siano le proprietà .

Quando sto introducendo modifiche al codice, non mi interessa l'isolamento dei test - so già che "l'errore" è da qualche parte nel mio attuale stack di modifiche non verificate. Se eseguo i test frequentemente, allora limito la profondità di quello stack e trovare l'errore è banale (in casi estremi, i test vengono eseguiti dopo ogni modifica - la profondità massima dello stack è una). Ma l'esecuzione dei test non è l'obiettivo - è un'interruzione - quindi c'è valore nel ridurre l'impatto dell'interruzione. Un modo per ridurre l'interruzione è garantire che i test siano veloci ( Gary Bernhardt suggerisce 300 ms , ma non ho capito come farlo nelle mie circostanze).

Se invocare Calculator::addnon aumenta significativamente il tempo necessario per eseguire il test (o una qualsiasi delle altre proprietà importanti per questo caso d'uso), allora non mi preoccuperei di usare un doppio test - non offre vantaggi che superano i costi .

Si noti qui i due presupposti: un essere umano come parte della valutazione dei costi e la breve serie di cambiamenti non verificati nella valutazione dei benefici. In circostanze in cui tali condizioni non valgono, il valore di "isolamento" cambia un po '.

Vedi anche Hot Lava , di Harry Percival.


5
una cosa che aggiunge beffardo è dimostrare che la calcolatrice può essere derisa, cioè che il design non abbina persona e calcolatrice (anche se questo può essere verificato anche in altri modi)
jk.

40

Come dovrebbero essere scritti esattamente i test unitari senza deridere ampiamente?

Riducendo al minimo gli effetti collaterali nel codice.

Prendendo il tuo codice di esempio, se calculatorad esempio parla con un'API Web, o crei test fragili che si basano sulla capacità di interagire con quell'API Web o ne crei una derisione. Se tuttavia è un insieme deterministico e privo di stato di funzioni di calcolo, allora non lo devi (e non dovresti) deridere. Se lo fai, rischi che il tuo finto si comporti in modo diverso dal codice reale, portando a bug nei tuoi test.

I mock dovrebbero essere necessari solo per il codice che legge / scrive nel file system, nei database, negli endpoint URL ecc .; che dipendono dall'ambiente in cui si esegue; o che sono altamente statali e di natura non deterministica. Quindi se tenete al minimo quelle parti del codice e le nascondete dietro le astrazioni, allora sono facili da deridere e il resto del codice evita la necessità di derisioni.

Per i punti di codice che hanno effetti collaterali, vale la pena scrivere test che deridono e test che non lo fanno. Questi ultimi però hanno bisogno di cure poiché saranno intrinsecamente fragili e possibilmente lenti. Quindi potresti volerli solo dire durante la notte su un server CI, piuttosto che ogni volta che salvi e costruisci il tuo codice. I primi test dovrebbero essere eseguiti il ​​più spesso possibile. Se ogni test è quindi un'unità o un test di integrazione diventa accademico ed evita "guerre di fiamma" su ciò che è e non è un test unitario.


8
Questa è la risposta corretta sia nella pratica che in termini di schivare un dibattito semantico insignificante.
Jared Smith,

Hai un esempio di una base di codice open source non banale che utilizza un tale stile e ottiene ancora una buona copertura del test?
Joeri Sebrechts,

4
@JoeriSebrechts ogni singolo FP? Esempio
Jared Smith,

Non proprio quello che sto cercando, in quanto è solo una raccolta di funzioni indipendenti l'una dall'altra, non un sistema di funzioni che si chiamano l'un l'altro. Come evitare di dover costruire argomenti complessi per una funzione allo scopo di testarla, se quella funzione è una di quelle di livello superiore? Ad esempio il ciclo principale di un gioco.
Joeri Sebrechts,

1
@JoeriSebrechts hmm, o sto fraintendendo quello che vuoi, o non hai approfondito abbastanza l'esempio che ho dato. Le funzioni ramda usano chiamate interne in tutto il luogo nella loro fonte (ad es R.equals.). Poiché queste sono per lo più funzioni pure, generalmente non vengono prese in giro nei test.
Jared Smith,

31

Queste domande sono abbastanza diverse nella loro difficoltà. Prendiamo prima la domanda 2.

I test unitari e i test di integrazione sono chiaramente separati. Un test di unità verifica un'unità (metodo o classe) e utilizza altre unità solo quanto necessario per conseguire tale obiettivo. La derisione può essere necessaria, ma non è il punto del test. Un test di integrazione verifica l'interazione tra diverse unità effettive . Questa differenza è l'intero motivo per cui abbiamo bisogno sia dei test unitari che di quelli di integrazione: se uno svolgesse il compito dell'altro abbastanza bene, non lo faremmo, ma si scopre che di solito è più efficiente utilizzare due strumenti specializzati anziché uno strumento generalizzato .

Ora per la domanda importante: come dovresti testare l'unità? Come detto sopra, i test unitari dovrebbero costruire strutture ausiliarie solo per quanto necessario . Spesso è più semplice utilizzare un database simulato rispetto al proprio database reale o persino a qualsiasi database reale. Tuttavia, deridere in sé non ha valore. Se spesso accade che in effetti è più semplice utilizzare componenti effettivi di un altro livello come input per un test unitario di livello medio. In tal caso, non esitate a usarli.

Molti praticanti temono che se il test unitario B riutilizza le classi che erano già state testate dal test unitario A, allora un difetto nell'unità A provoca fallimenti del test in più punti. Io considerare questo non è un problema: una suite di test deve riuscire al 100% in modo da darvi la rassicurazione che vi serve, quindi non è un grosso problema di avere troppi fallimenti - dopo tutto, è fare avere un difetto. L'unico problema critico sarebbe se un difetto scatenato troppo pochi fallimenti.

Pertanto, non fare una religione di derisione. È un mezzo, non un fine, quindi se riesci a cavartela evitando lo sforzo extra, dovresti farlo.


4
The only critical problem would be if a defect triggered too few failures.questo è uno dei punti deboli del deridere. Dobbiamo "programmare" il comportamento previsto, quindi potremmo fallire nel farlo, facendo finire i nostri test come "falsi positivi". Ma il deridere è una tecnica molto utile per raggiungere il determinismo (la condizione più importante del test). Li uso in tutti i miei progetti, quando possibile. Mi mostrano anche quando l'integrazione è troppo complessa o la dipendenza troppo stretta.
Laiv,

1
Se un'unità in fase di test utilizza altre unità, non è davvero un test di integrazione? Perché in sostanza questa unità testerebbe l'interazione tra dette unità, esattamente come farebbe un test di integrazione.
Alexander Lomia,

11
@AlexanderLomia: Come chiameresti un'unità? Chiameresti anche 'String' un'unità? Lo farei, ma non mi sognerei di prenderlo in giro.
Bart van Ingen Schenau,

5
" I test unitari e i test di integrazione sono chiaramente separati. Un test unitario verifica un'unità (metodo o classe) e utilizza altre unità solo quanto necessario per raggiungere tale obiettivo ". Ecco il problema. Questa è la tua definizione di unit test. Il mio è abbastanza diverso. Quindi la distinzione tra loro è solo "chiaramente separata" per ogni data definizione ma la separazione varia tra le definizioni.
David Arno,

4
@Voo Avendo lavorato con tali codebase, mentre può essere fastidioso trovare il problema originale (specialmente se l'architettura ha dipinto le cose che useresti per eseguire il debug), ho ancora avuto più problemi da derisioni che hanno causato il i test per continuare a funzionare dopo la rottura del codice utilizzato per il test.
James_pic,

6

OK, quindi per rispondere direttamente alle tue domande:

Come devono essere scritti correttamente i test unitari?

Come dici tu, dovresti deridere le dipendenze e testare solo l'unità in questione.

Dove sta esattamente la linea che li separa dai test di integrazione?

Un test di integrazione è un test unitario in cui le dipendenze non vengono prese in giro.

Un test che verifica il metodo Person.calculate senza deridere il calcolatore può essere considerato un test unitario?

No. Devi inserire la dipendenza della calcolatrice in questo codice e puoi scegliere tra una versione derisa o una reale. Se ne usi uno deriso è un test unitario, se ne usi uno vero è un test di integrazione.

Tuttavia, un avvertimento. ti interessa davvero come la gente pensa che i tuoi test dovrebbero essere chiamati?

Ma la tua vera domanda sembra essere questa:

una rapida ricerca di Google sul deridere rivela tonnellate di articoli che affermano che "il deridere è un odore di codice" e che dovrebbero essere per lo più (sebbene non completamente) evitati.

Penso che il problema qui sia che molte persone usano le beffe per ricreare completamente le dipendenze. Ad esempio potrei deridere la calcolatrice nel tuo esempio come

public class MockCalc : ICalculator
{
     public Add(int a, int b) { return 4; }
}

Non farei qualcosa del genere:

myMock = Mock<ICalculator>().Add((a,b) => {return a + b;})
myPerson.Calculate()
Assert.WasCalled(myMock.Add());

Direi che sarebbe "testare la mia simulazione" o "testare l'implementazione". Direi " Non scrivere Mock! * Così ".

Altre persone non sarebbero d'accordo con me, avremmo iniziato enormi guerre di fiamma sui nostri blog sul modo migliore per deridere, il che non avrebbe davvero senso se non avessi compreso l'intero background dei vari approcci e davvero non offrissi molto valore a qualcuno che vuole solo scrivere buoni test.


Grazie per una risposta esaustiva. Per quanto riguarda quello che gli altri pensano dei miei test, in realtà voglio evitare di scrivere test di semi-integrazione e semi-unità che tendono a diventare un disordine inarrestabile man mano che il progetto avanza.
Alexander Lomia,

nessun problema, penso che il problema sia che le definizioni delle due cose non sono concordate al 100% da tutti.
Ewan,

Rinominerei la tua classe MockCalcin StubCalc, e la chiamerei un troncone non un finto. martinfowler.com/articles/…
bdsl

@bdsl Quell'articolo ha 15 anni
Ewan,

4
  1. Come devono essere implementati correttamente i test unitari?

La mia regola empirica è che i test unitari corretti:

  • Sono codificati rispetto alle interfacce, non alle implementazioni . Questo ha molti vantaggi. Innanzitutto, assicura che le tue classi seguano il principio di inversione di dipendenza da SOLID . Inoltre, questo è ciò che fanno le altre classi ( giusto? ), Quindi i tuoi test dovrebbero fare lo stesso. Inoltre, ciò consente di testare più implementazioni della stessa interfaccia riutilizzando gran parte del codice di test (solo l'inizializzazione e alcune asserzioni potrebbero cambiare).
  • Sono autonomi . Come hai detto, le modifiche a qualsiasi codice esterno non possono influire sul risultato del test. Pertanto, i test unitari possono essere eseguiti al momento della compilazione. Ciò significa che hai bisogno di beffe per rimuovere eventuali effetti collaterali. Tuttavia, se stai seguendo il principio di inversione di dipendenza, questo dovrebbe essere relativamente semplice. Buoni framework di test come Spock possono essere utilizzati per fornire dinamicamente implementazioni simulate di qualsiasi interfaccia da utilizzare nei test con una codifica minima. Ciò significa che ogni classe di test deve solo esercitare il codice da una sola classe di implementazione, oltre al framework di test (e forse alle classi di modello ["bean"]).
  • Non richiede un'applicazione in esecuzione separata . Se il test deve "parlare con qualcosa", sia esso un database o un servizio Web, è un test di integrazione, non un test unitario. Traccio la linea sulle connessioni di rete o sul filesystem. Un database SQLite puramente in memoria, ad esempio, è un gioco equo secondo me per un test unitario se ne hai davvero bisogno.

Se ci sono classi di utilità da framework che complicano il test unitario, potresti persino trovare utile creare interfacce e classi "wrapper" molto semplici per facilitare il derisione di tali dipendenze. Questi involucri non sarebbero quindi necessariamente soggetti a test unitari.

  1. Dove si trova esattamente la linea tra loro [unit test] e test di integrazione?

Ho trovato questa distinzione come la più utile:

  • I test unitari simulano il "codice utente" , verificando il comportamento delle classi di implementazione rispetto al comportamento desiderato e alla semantica delle interfacce a livello di codice .
  • I test di integrazione simulano l'utente , verificando il comportamento dell'applicazione in esecuzione rispetto a casi d'uso specifici e / o API formali. Per un servizio Web, "utente" sarebbe un'applicazione client.

C'è un'area grigia qui. Ad esempio, se è possibile eseguire un'applicazione in un contenitore Docker ed eseguire i test di integrazione come fase finale di una build e successivamente distruggere il contenitore, è corretto includere tali test come "test unitari"? Se questo è il tuo acceso dibattito, sei in un posto abbastanza buono.

  1. È vero che praticamente ogni test unitario deve essere deriso?

No. Alcuni casi di test individuali riguarderanno condizioni di errore, come passare nullcome parametro e verificare di ottenere un'eccezione. Molti test del genere non richiedono alcuna derisione. Inoltre, le implementazioni che non hanno effetti collaterali, ad esempio l'elaborazione di stringhe o le funzioni matematiche, potrebbero non richiedere simulazioni poiché si verifica semplicemente l'output. Ma la maggior parte delle classi che vale la pena avere, credo, richiederà almeno una derisione da qualche parte nel codice di test. (Meno è, meglio è.)

Il problema "odore di codice" che hai citato sorge quando hai una classe troppo complicata, che richiede un lungo elenco di dipendenze simulate per scrivere i tuoi test. Questo è un indizio che devi riformattare l'implementazione e dividere le cose, in modo che ogni classe abbia un footprint più piccolo e una responsabilità più chiara, e quindi sia più facilmente verificabile. Ciò migliorerà la qualità a lungo termine.

Solo un test unitario dovrebbe superare un bug nell'unità testata.

Non penso che questa sia un'aspettativa ragionevole, perché funziona contro il riutilizzo. Potresti avere un privatemetodo, ad esempio, che viene chiamato da più publicmetodi pubblicati dalla tua interfaccia. Un bug introdotto in quel metodo potrebbe quindi causare più errori di test. Questo non significa che dovresti copiare lo stesso codice in ogni publicmetodo.


3
  1. Non dovrebbero interrompere alcuna modifica di codice non correlata altrove nella base di codice.

Non sono davvero sicuro di come questa regola sia utile. Se un cambiamento in una classe / metodo / qualunque cosa può interrompere il comportamento di un'altra nel codice di produzione, le cose sono, in realtà, collaboratori e non indipendenti. Se i test falliscono e il codice di produzione no, i test sono sospetti.

  1. Solo un test unitario dovrebbe superare un bug nell'unità testata, al contrario dei test di integrazione (che possono rompersi in cumuli).

Considererei questa regola anche con sospetto. Se sei davvero abbastanza bravo da strutturare il tuo codice e scrivere i tuoi test in modo tale che un bug causi esattamente un test unit unit, allora stai dicendo che hai già identificato tutti i potenziali bug, anche quando la base di codice si evolve per usare i casi non ho anticipato.

Dove sta esattamente la linea che li separa dai test di integrazione?

Non credo sia una distinzione importante. Che cos'è comunque una 'unità' di codice?

Prova a trovare i punti di ingresso in cui puoi scrivere test che semplicemente "hanno senso" in termini di dominio / regole aziendali problematiche con cui si occupa quel livello di codice. Spesso questi test sono in qualche modo "funzionali" in natura - inserisci un input e verifica che l'output sia come previsto. Se i test esprimono il comportamento desiderato del sistema, spesso rimangono abbastanza stabili anche quando il codice di produzione si evolve e viene refactored.

Come dovrebbero essere scritti esattamente i test unitari senza deridere ampiamente?

Non leggere troppo la parola "unità" e tendersi a usare le tue classi di produzione reali nei test, senza preoccuparti troppo se coinvolgi più di una di esse in un test. Se uno di questi è difficile da usare (perché richiede molta inizializzazione o deve colpire un vero database / server di posta elettronica ecc.), Allora lascia che i tuoi pensieri diventino beffardi / falsi.


" Cos'è comunque una" unità "di codice? " Ottima domanda che può avere risposte inaspettate che potrebbero persino dipendere da chi sta rispondendo. In genere, la maggior parte delle definizioni dei test unitari li spiega come relativi a un metodo o a una classe, ma in tutti i casi questa non è una misura davvero utile di "unità". Se ho un Person:tellStory()metodo incorpora i dettagli di una persona in una stringa, quindi la restituisce, quindi la "storia" è probabilmente un'unità. Se creo un metodo di supporto privato che nasconde parte del codice, non credo di aver introdotto una nuova unità, non ho bisogno di provarlo separatamente.
VLAZ,

2

Innanzitutto, alcune definizioni:

Un test unitario testa le unità in modo isolato dalle altre unità, ma ciò significa che non è concretamente definito da alcuna fonte autorevole, quindi definiamolo un po 'meglio: se i confini dell'I / O vengono attraversati (se quell'I / O è di rete, disco, schermo o input dell'interfaccia utente), esiste un punto semi-obiettivo in cui possiamo tracciare una linea. Se il codice dipende dall'I / O, sta attraversando un confine dell'unità e quindi dovrà deridere l'unità responsabile di quell'I / O.

In base a tale definizione, non vedo una ragione convincente per deridere cose come le funzioni pure, il che significa che il test unitario si presta a funzioni pure o funzioni senza effetti collaterali.

Se vuoi unità di test unit con effetti, le unità responsabili degli effetti dovrebbero essere derise, ma forse dovresti prendere in considerazione un test di integrazione. Quindi, la risposta breve è: "se hai bisogno di deridere, chiediti se quello di cui hai veramente bisogno è un test di integrazione". Ma qui c'è una risposta migliore, più lunga, e la tana del coniglio diventa molto più profonda. Le simulazioni possono essere il mio odore di codice preferito perché c'è così tanto da imparare da loro.

Odori di codice

Per questo, ci rivolgeremo a Wikipedia:

Nella programmazione del computer, un odore di codice è una caratteristica del codice sorgente di un programma che potrebbe indicare un problema più profondo.

Continua più tardi ...

"Gli odori sono determinate strutture nel codice che indicano una violazione dei principi fondamentali del design e influiscono negativamente sulla qualità del design". Suryanarayana, Girish (novembre 2014). Refactoring per gli odori di progettazione del software. Morgan Kaufmann. p. 258.

Gli odori di codice di solito non sono bug; non sono tecnicamente errati e non impediscono il funzionamento del programma. Invece, indicano debolezze nella progettazione che potrebbero rallentare lo sviluppo o aumentare il rischio di bug o guasti in futuro.

In altre parole, non tutti gli odori di codice sono cattivi. Invece, sono indicazioni comuni che qualcosa potrebbe non essere espresso nella sua forma ottimale e l'odore può indicare un'opportunità per migliorare il codice in questione.

In caso di derisione, l'odore indica che le unità che sembrano chiamare derisioni dipendono dalle unità da deridere. Potrebbe essere un'indicazione che non abbiamo scomposto il problema in pezzi atomicamente risolvibili e che potrebbe indicare un difetto di progettazione nel software.

L'essenza di tutto lo sviluppo del software è il processo di scomporre un grosso problema in pezzi più piccoli e indipendenti (decomposizione) e comporre le soluzioni insieme per formare un'applicazione che risolva il grande problema (composizione).

La derisione è necessaria quando le unità utilizzate per suddividere il problema di grandi dimensioni in parti più piccole dipendono l'una dall'altra. Detto in altro modo, è necessario prendere in giro quando le nostre presunte unità atomiche di composizione non sono realmente atomiche e la nostra strategia di decomposizione non è riuscita a scomporre il problema più grande in problemi più piccoli e indipendenti da risolvere.

Ciò che rende il deridere un odore di codice non è che ci sia qualcosa di intrinsecamente sbagliato nel deridere - a volte è molto utile. Ciò che lo rende un odore di codice è che potrebbe indicare una fonte problematica di accoppiamento nella tua applicazione. A volte rimuovere quella fonte di accoppiamento è molto più produttivo che scrivere una finta.

Esistono molti tipi di accoppiamento e alcuni sono migliori di altri. Capire che le beffe sono un odore di codice può insegnarti a identificare ed evitare i peggiori tipi all'inizio del ciclo di vita del design dell'applicazione, prima che l'odore diventi qualcosa di peggio.


1

Il derisione dovrebbe essere usato solo come ultima risorsa, anche nei test unitari.

Un metodo non è un'unità e persino una classe non è un'unità. Un'unità è una separazione logica del codice che ha senso, indipendentemente da come la chiami. Un elemento importante di avere un codice ben collaudato è la possibilità di refactoring liberamente e parte della capacità di refactoring libero significa che non è necessario modificare i test per farlo. Più deridi, più devi cambiare i tuoi test quando esegui il refactoring. Se si considera il metodo come unità, è necessario modificare i test ogni volta che si esegue il refactoring. E se consideri la classe come unità, allora devi cambiare i tuoi test ogni volta che vuoi dividere una classe in più classi. Quando devi riformattare i tuoi test per riformattare il tuo codice, le persone scelgono di non refactificare il loro codice, che è solo la cosa peggiore che può accadere a un progetto. È essenziale che tu possa dividere una classe in più classi senza dover riformattare i tuoi test o finirai con lezioni di spaghetti di 500 linee di grandi dimensioni. Se stai trattando metodi o classi come unità con test unitari, probabilmente non stai eseguendo la programmazione orientata agli oggetti ma una sorta di programmazione funzionale mutante con gli oggetti.

L'isolamento del codice per un test unitario non significa che deridi tutto al di fuori di esso. Se lo facesse, dovresti deridere la lezione di matematica della tua lingua e assolutamente nessuno pensa che sia una buona idea. Le dipendenze interne non dovrebbero essere trattate diversamente dalle dipendenze esterne. Ti fidi che sono ben testati e funzionano come dovrebbero. L'unica vera differenza è che se le tue dipendenze interne stanno rompendo i tuoi moduli, puoi interrompere ciò che stai facendo per risolverlo piuttosto che dover pubblicare un problema su GitHub e scavare in una base di codice che non capisci per risolverlo o spero per il meglio.

Isolare il tuo codice significa solo trattare le tue dipendenze interne come scatole nere e non testare le cose che stanno accadendo al loro interno. Se hai il modulo B che accetta input di 1, 2 o 3 e hai il modulo A, che lo chiama, non hai i test per il modulo A che eseguono ciascuna di queste opzioni, basta sceglierne uno e utilizzarlo. Significa che i test per il Modulo A dovrebbero testare i diversi modi in cui trattate le risposte dal Modulo B, non le cose che vi passano.

Quindi, se il tuo controller passa un oggetto complesso a una dipendenza e quella dipendenza fa diverse cose possibili, magari salvandola nel database e forse restituendo una varietà di errori, ma tutto ciò che fa effettivamente il tuo controller è semplicemente controllare se ritorna un errore o meno e passare tali informazioni, quindi tutto ciò che si verifica nel controller è un test per se restituisce un errore e lo supera e un test per se non restituisce un errore. Non si verifica se qualcosa è stato salvato nel database o che tipo di errore è l'errore, perché sarebbe un test di integrazione. Non è necessario prendere in giro la dipendenza per farlo. Hai isolato il codice.

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.