In che modo esattamente i test delle unità dovrebbero essere scritti senza fare il mocking estensivamente?

64

Come ho capito, il punto dei test unitari è quello di testare unità di codice in isolamento . Ciò significa che:

  1. Non dovrebbero interrompere il codice non correlato cambiare altrove nel codebase.
  2. Solo un test di unità dovrebbe essere superato da un bug nell'unità testata, al contrario dei test di integrazione (che potrebbero rompersi in heap).

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 rete, file system, database, ecc.

Ciò porta a una conclusione logica, che praticamente ogni test di unità ha bisogno di prendere in giro . D'altra parte, una rapida ricerca su Google che deride rivela tonnellate di articoli che affermano che "il beffardo è un odore di codice" e che dovrebbe essere evitato (anche se non completamente).

Ora, alle domande.

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

Aggiornamento 1

Prendi in considerazione 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'
    }
}

Può un test che verifica il metodo Person.calculate senza prendere in giro la dipendenza Calculator (dato che la Calculator è una classe leggera che non accede al "mondo esterno") può essere considerata un test unitario?

    
posta Alexander Lomia 27.11.2018 - 11:46
fonte

6 risposte

52

the point of unit tests is to test units of code in isolation.

Martin Fowler su Test unitario

Unit testing is often talked about in software development, and is a term that I've been familiar with during my whole time writing programs. Like most software development terminology, however, it's very ill-defined, and I see confusion can often occur when people think that it's more tightly defined than it actually is.

Che Kent Beck ha scritto in Sviluppo guidato dal test, per esempio

I call them "unit tests", but they don't match the accepted definition of unit tests very well

Qualsiasi affermazione del "punto di test unitario" dipenderà in larga misura dalla definizione di "test unitario".

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

I consigli contraddittori che vedi provengono da persone che operano con un diverso insieme di ipotesi.

Ad esempio, se stai scrivendo test per supportare gli sviluppatori durante il processo di refactoring, e dividere una unità in due è un refactoring che dovrebbe essere supportato, quindi 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:

Can a test that tests the Person.calculate method without mocking the Calculator dependency (given, that the Calculator is a lightweight class that does not access "the outside world") be considered a unit test?

Penso che sia la domanda sbagliata da porre; è ancora una discussione su labels , quando credo che ciò a cui teniamo veramente sono properties .

Quando introduco 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 spesso i test, limito la profondità di tale stack e trovando l'errore è banale (nel caso estremo, i test vengono eseguiti dopo ogni modifica - la profondità massima dello stack è uno). Ma eseguire i test non è l'obiettivo - è un'interruzione - quindi c'è un valore nel ridurre l'impatto dell'interruzione. Un modo per ridurre l'interruzione è garantire che i test siano rapidi ( Gary Bernhardt suggerisce 300 ms , ma non ho capito come farlo nelle mie circostanze).

Se invocare Calculator::add non aumenta significativamente il tempo richiesto per eseguire il test (o altre proprietà importanti per questo caso d'uso), allora non mi preoccuperei di usare un test double - non lo fa fornire benefici che vanno fuori dai costi.

Notate qui le due ipotesi: un essere umano come parte della valutazione dei costi e lo short stack di cambiamenti non verificati nella valutazione dei benefici. Nelle circostanze in cui tali condizioni non reggono, il valore di "isolamento" cambia un po '.

Vedi anche Hot Lava , di Harry Percival.

    
risposta data 27.11.2018 - 12:47
fonte
32

How exactly should unit tests be written without mocking extensively?

Riducendo al minimo gli effetti collaterali nel codice.

Prendendo il tuo codice di esempio, se calculator , ad esempio, parla con un'API Web, allora crei test fragili che si basano sulla possibilità di interagire con tale API Web o di crearne uno. Se tuttavia è un insieme deterministico, senza stato di funzioni di calcolo, allora non lo si (e non dovrebbe) deriderlo. Se lo fai, rischi che il tuo mock si comporta in modo diverso rispetto al codice reale, portando a bug nei test.

I mock dovrebbero essere necessari solo per il codice che legge / scrive sul file system, i database, gli endpoint URL, ecc; che dipendono dall'ambiente in cui stai correndo; o che sono di natura altamente statica e non deterministica. Quindi se mantieni al minimo queste parti del codice e le nascondi dietro alle astrazioni, allora sono facili da deridere e il resto del codice evita la necessità di fare il mock.

Per i punti di codice che hanno effetti collaterali, vale la pena scrivere test che simulano e test che non lo fanno. Quest'ultimo però ha 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 però dovrebbero essere eseguiti il più spesso possibile. Per quanto riguarda il fatto che ogni test sia quindi un'unità o un test di integrazione diventa accademico ed evita "guerre di fiamma" su ciò che è e non è un test unitario.

    
risposta data 27.11.2018 - 13:34
fonte
30

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

Test unitari e test di integrazione sono chiaramente separati. Un unit test testa one (metodo o classe) e usa altre unità solo quanto necessario per raggiungere quell'obiettivo. Potrebbe essere necessario un derisione, ma non è il punto del test. Un test di integrazione verifica l'interazione tra le diverse unità effettive . Questa differenza è l'intera ragione per cui abbiamo bisogno di test di unità e di integrazione - se uno facesse il lavoro dell'altro abbastanza bene, non lo faremmo, ma è risultato che è solitamente più efficiente usare due strumenti specializzati piuttosto che uno strumento generalizzato.

Ora per la domanda importante: come si dovrebbe testare l'unità? Come detto sopra, i test unitari dovrebbero costruire strutture ausiliarie solo per quanto necessario . Spesso è più facile usare un database di simulazione che il tuo vero database o persino un qualsiasi database reale. Tuttavia, il derisione in sé non ha valore. Se spesso accade che è in effetti più facile usare componenti effettivi di un altro strato come input per un test di unità di medio livello. Se è così, non esitate a usarli.

Molti professionisti temono che se il test unitario B riutilizza le classi che sono già state testate dal test unitario A, allora un difetto nell'unità A causa fallimenti del test in più punti. Non considero questo un problema: una suite di test deve avere successo 100% per darti la rassicurazione di cui hai bisogno, quindi non è un grosso problema avere troppi fallimenti - dopotutto, tu < em> do hanno un difetto. L'unico problema critico sarebbe se un difetto innescasse troppo pochi fallimenti.

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

    
risposta data 27.11.2018 - 11:59
fonte
4

OK, quindi rispondi direttamente alle tue domande:

How should unit tests be written properly?

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

Where exactly does the line between them and integration tests lie?

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

Can a test that tests the Person.calculate method without mocking the Calculator be considered a unit test?

No. È necessario iniettare la dipendenza della calcolatrice in questo codice e si può scegliere tra una versione derisoria o una versione reale. Se utilizzi un mocked è un test unitario, se ne usi uno reale è un test di integrazione.

Tuttavia, un avvertimento. ti importa davvero di ciò che la gente pensa che i tuoi test dovrebbero essere chiamati?

Ma la tua vera domanda sembra essere questa:

a quick Google search about mocking reveals tons of articles that claim that "mocking is a code smell" and should mostly (though not completely) be avoided.

Penso che il problema qui sia che molte persone usano i mock per ricreare completamente le dipendenze. Per esempio potrei prendere in giro la calcolatrice nel tuo esempio come

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

Non vorrei fare qualcosa di simile:

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! * in questo modo".

Le altre persone non sarebbero d'accordo con me, inizieremmo massicce guerre di fiamma sui nostri blog sul modo migliore di deridere, il che non avrebbe alcun senso se non avessi compreso l'intero background dei vari approcci e davvero non offrirei un intero molto valore a qualcuno che vuole solo scrivere buoni test.

    
risposta data 27.11.2018 - 13:12
fonte
3
  1. How should unit tests be implemented properly?

La mia regola empirica è quella dei test unitari appropriati:

  • Sono codificati contro interfacce, non implementazioni . Questo ha molti vantaggi. Per esempio, assicura che le tue classi seguano il principio di inversione delle dipendenze da SOLID . Inoltre, questo è ciò che fanno le altre classi ( giusto? ) così i tuoi test dovrebbero fare lo stesso. Inoltre, questo ti permette di testare più implementazioni della stessa interfaccia mentre riusando gran parte del codice di test (solo l'inizializzazione e alcune asserzioni cambierebbero).
  • Sono autonomi . Come hai detto, i cambiamenti in qualsiasi codice esterno non possono influenzare il risultato del test. Pertanto, i test unitari possono essere eseguiti in fase di costruzione. Ciò significa che hai bisogno di mock per rimuovere eventuali effetti collaterali. Tuttavia, se stai seguendo il principio di inversione delle dipendenze, questo dovrebbe essere relativamente facile. Buoni framework di test come Spock possono essere utilizzati per fornire in modo dinamico implementazioni simulate di qualsiasi interfaccia da utilizzare nei test con codifica minima. Ciò significa che ogni classe di test ha solo bisogno di esercitare il codice da esattamente una classe di implementazione, oltre al framework di test (e magari classi di modelli ["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 alle connessioni di rete o al filesystem. Un database SQLite puramente in memoria, ad esempio, è un gioco equo a mio parere per un test di unità se ne hai davvero bisogno.

Se ci sono classi di utilità da framework che complicano il testing delle unità, potresti anche trovare utile creare interfacce e classi "wrapper" molto semplici per facilitare il deridere di quelle dipendenze. Questi involucri non sarebbero quindi soggetti a test unitari.

  1. Where exactly does the line between them [unit tests] and integration tests lie?

Ho trovato che questa distinzione è la più utile:

  • I test unitari simulano il "codice utente" , verificando il comportamento delle classi di implementazione rispetto al comportamento e alla semantica desiderati delle interfacce a livello di codice .
  • 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 di Docker ed eseguire i test di integrazione come fase finale di una build e successivamente distruggere il contenitore, è corretto includere tali test come "unit test"? Se questo è il tuo acceso dibattito, sei in un bel posto.

  1. Is it true that virtually every unit test needs to mock?

No. Alcuni singoli casi di test riguarderanno condizioni di errore, ad esempio il passaggio di null come parametro e la verifica di un'eccezione. Un sacco di test del genere non richiedono alcun mock. Inoltre, le implementazioni che non hanno effetti collaterali, ad esempio l'elaborazione delle stringhe o le funzioni matematiche, potrebbero non richiedere alcun mock perché si verifica semplicemente l'output. Ma la maggior parte delle classi che vale la pena avere, credo, richiederà almeno un mock da qualche parte nel codice di test. (Meno ce ne sono, meglio è.)

Il problema dell'odore di codice che hai menzionato sorge quando hai una classe che è eccessivamente complicata, che richiede una lunga lista di dipendenze fittizie per scrivere i tuoi test. Questo è un indizio del fatto che è necessario ridefinire l'implementazione e suddividere le cose, in modo che ogni classe abbia un'impronta più piccola e una responsabilità più chiara, ed è quindi più facilmente verificabile. Ciò migliorerà la qualità a lungo termine.

Only one unit test should break by a bug in the tested unit.

Non penso che questa sia un'aspettativa ragionevole, perché funziona contro il riutilizzo. Ad esempio, potresti avere un metodo private , chiamato da più metodi public 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 metodo public .

    
risposta data 28.11.2018 - 00:28
fonte
3
  1. They should not break by any unrelated code change elsewhere in the codebase.

Non sono proprio sicuro di come sia utile questa regola. Se un cambiamento in una classe / metodo / qualsiasi cosa può rompere il comportamento di un altro nel codice di produzione, allora le cose sono, in realtà, collaboratori e non indipendenti. Se i tuoi test si interrompono e il tuo codice di produzione no, allora i tuoi test sono sospetti.

  1. Only one unit test should break by a bug in the tested unit, as opposed to integration tests (which may break in heaps).

Anch'io considero questa regola con sospetto. Se sei veramente bravo a strutturare il tuo codice e a scrivere i tuoi test in modo tale che un bug causi esattamente un fallimento del test di un'unità, allora stai dicendo che hai identificato tutti i potenziali bug già, anche se il codebase si evolve per usare i casi non ho anticipato.

Where exactly does the line between them and integration tests lie?

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

Prova a trovare i punti di ingresso in cui puoi scrivere test che hanno un 'senso' in termini di dominio del problema / regole di business che quel livello del codice sta trattando. Spesso questi test sono in qualche modo "funzionali" in natura - inseriscono un input e verificano che l'output sia come previsto. Se i test esprimono un comportamento desiderato del sistema, spesso rimangono piuttosto stabili anche quando il codice di produzione si evolve e viene refactored.

How exactly should unit tests be written without mocking extensively?

Non leggere troppo il termine "unità" e inclinarti a utilizzare le tue vere classi di produzione nei test, senza preoccuparti troppo se ne coinvolgi più di uno in un test. Se uno di questi è difficile da usare (perché richiede molto inizializzazione, o ha bisogno di colpire un vero database / server di posta elettronica, ecc.), Allora lascia che i tuoi pensieri si trasformino in beffardi / finti.

    
risposta data 28.11.2018 - 00:55
fonte

Leggi altre domande sui tag