È buona norma avere un test unitario per un semplice metodo di dati?

-1

Ho un semplice metodo di dati che fa questo:

public void Write(Foo foo)
{
  db.Foos.Add(foo);
  db.SaveChanges();
}

Mi è stato chiesto di scrivere test unitari per questo. Per fare ciò, ho dovuto creare un falso DbContext e DbSet. In pratica sto solo affermando che Add() è stato chiamato su DbSet e che Save() è stato chiamato su DbContext.

La mia preoccupazione è che accoppiato troppo strettamente il test al codice. Il test sta essenzialmente dettando come il codice fa il suo lavoro, invece di cosa fa.

Sto solo cercando un assegno sanitario qui. Ho sbagliato? È bello avere test come questo?

    
posta Bob Horn 07.04.2018 - 19:34
fonte

5 risposte

5

In definitiva i test sono uno strumento. Come tutti gli strumenti, c'è un costo e un possibile beneficio. In questo caso, il test ha un costo elevato (il test sta dettando i dettagli dell'implementazione e si interromperà se l'implementazione cambia, impedendo il refactoring) e un basso beneficio (il codice che stai testando è così semplice che è banalmente verificabile), quindi non posso giustificare questo test.

Non scrivo test per metodi banali, perché posso essere sicuro che funzionano solo guardandoli. E anche se non funzionano, probabilmente vengono comunque utilizzati in un contesto più ampio e, se stai testando i comportamenti piuttosto che i metodi, gli altri test dovrebbero individuare il problema.

Mantra come "è necessario raggiungere una copertura del 100% del codice" o "è necessario scrivere un test per ogni metodo" sono mal concepiti e si oppongono agli ingegneri produttivi che stanno cercando di fornire un codice di qualità che promuova gli obiettivi dell'organizzazione.

To do so, I had to create a fake DbContext and DbSet. I'm basically just asserting that Add() was called on the DbSet, and that Save() was called on the DbContext.

My concern is that it's too tightly coupling the test to the code. The test is basically dictating how the code does its job, instead of what it does.

Questo è esattamente corretto. Un test del genere non sta asserendo un comportamento, ma piuttosto semplicemente ridimensionando il codice implementato al suo interno. Un test come questo asserisce che il codice non è cambiato, non che il comportamento non è cambiato.

Ad esempio, immagina se db.Foo avesse un altro metodo, chiamato AddAndSave(...) che gestiva entrambe le operazioni per te. Se hai sostituito la tua implementazione con una che chiama semplicemente questo metodo AddAndSave , il test fallirebbe, eppure il comportamento è lo stesso. Ancora peggio, immagina se db ha un metodo UndoLastSave() che ha annullato la precedente chiamata di salvataggio. Potresti aggiungere questo alla fine del tuo metodo e il test passerebbe comunque anche se il tuo metodo non ha più il comportamento desiderato.

Di solito, quando vedo persone che scrivono questo tipo di test, è un sintomo di test con la granularità errata. Si afferma comunemente che i test unitari devono testare solo un metodo, ma che è un interpretazione distorta dei test unitari. Il tuo sistema non deve essere isolato con un singolo metodo o classe, perché non c'è nulla che ti costringa a trattare quelli come la tua 'unità'. Vorrei invece provare a scrivere un test più lungo le linee di questo (scusate il mio pseudo-codice):

//given system.Clear() Foo foo = FooFactory.SomeFoo()

//when system.Write(foo)

//then assertTrue(system.Contains(foo))

L'idea qui è che vogliamo testare il comportamento della scrittura. Come è cambiato lo stato del mondo dopo che è stata chiamata la scrittura? Questo test chiama più di un metodo sul sistema e questo è fine .

    
risposta data 14.04.2018 - 16:02
fonte
6

La decisione se testare o meno un metodo si trova nella risposta alla domanda "Sei sicuro che il metodo funzioni nel modo in cui te lo aspetti?" Se la risposta è sì, non è richiesto nessun ulteriore test unitario.

Ovviamente, questa potrebbe non essere la tua decisione.

My concern is that it's too tightly coupling the test to the code.

I test unitari non devono essere disaccoppiati, tranne nella misura in cui si usano le interfacce per consentire l'uso di mock e stub. Il disaccoppiamento come tecnica è utile solo per il codice di produzione, non per i cablaggi di prova. L'imbracatura di prova cadrà comunque quando eseguirai la produzione.

To do so, I had to create a fake DbContext and DbSet. I'm basically just asserting that Add() was called on the DbSet, and that Save() was called on the DbContext.

Francamente, questo tipo di test unitari non è poi così interessante. Questo è solo un codice idraulico; non c'è alcuna funzionalità reale in fase di test, a parte il passaggio dei dati. Se un codice come questo cade perché non hai chiamato il metodo giusto, diventerà chiaramente ovvio quando eseguirai comunque i tuoi test di integrazione.

Se hai ancora intenzione di seguire questo percorso, e il numero di test che devi scrivere è molto ampio, considera la generazione del codice non solo i test ma anche il vero codice dell'impianto idraulico, oppure usa invece un repository generico.

    
risposta data 07.04.2018 - 22:50
fonte
2

Questo "metodo dei dati semplici" non è molto diverso da qualsiasi altro metodo che collaudi.

Come qualsiasi altro metodo, questo potrebbe avere regressioni. Ad esempio, cosa succede se qualcuno commenta erroneamente la seconda riga? Bene, il codice è ancora eseguito, nessuna eccezione viene lanciata, e hai un brutto bug di runtime che è abbastanza difficile da rintracciare una volta che inizia a verificarsi in produzione.

I test dovrebbero ridurre il rischio di introdurre quelle regressioni. Poiché questo è un codice aziendale, non importa quanto semplice, dovrebbe essere coperto. Nota, tuttavia, che:

  • Se questo è un metodo isolato (ovvero non hai lo stesso per Bar e Baz e centinaia di altri tipi), scrivere un test per questo dovrebbe essere estremamente semplice e diretto.

  • Se esistono centinaia di metodi che fanno esattamente lo stesso, ma per tipi diversi, pensa invece a utilizzare la generazione del codice. Non è necessario scrivere questo metodo manualmente, in primo luogo, e non testare il codice che non si scrive (testare il generatore di codice, tuttavia, è essenziale).

The test is basically dictating how the code does its job, instead of what it does.

Il test delle unità è un test white-box, quindi i test sono basati sul codice effettivo. Tuttavia, dovresti provare a pensare il test in termini funzionali. Qui, il metodo è troppo semplice per fare la differenza: l'esigenza funzionale è, quando I Write a Foo , per assicurare che Foo sia stato aggiunto con successo alla sequenza e che la modifica sia stata salvata. Non ci sono davvero molti modi per codificarlo o testarlo.

    
risposta data 07.04.2018 - 21:54
fonte
2

Fornisci pochissimo contesto.

Dovresti scrivere test che coprano l'intera "funzione" che stai testando e prendi in giro solo le dipendenze che rallentano i tuoi test (servizi web, file system, database).
Quando i test coprono il più possibile il codice di produzione effettivo, avrai la possibilità di ridefinire / ottimizzare / riprogettare il tuo codice senza toccare i test e avrai un rapido riscontro sulla correttezza della tua applicazione.

Hai ragione - il mocking DbContext accoppierà strettamente il tuo test ai dettagli di implementazione, in cui la modifica di un codice che non modifica il comportamento ti costringerà a cambiare anche i test. Ad esempio se decidi di utilizzare DbContext.Foos.AddRange(new[] {foo}) .

Non ci hai fornito un contesto completo: hai solo un metodo che salva un determinato oggetto nel database. Perché non hai altra logica da testare: devi testare che quell'oggetto dato sia stato effettivamente salvato nel database. È possibile eseguire test contro il database effettivo e chiamarlo test di "integrazione". Sulla base del framework che stai utilizzando puoi eseguire test su database "InMemory" che mantengono il test abbastanza velocemente.

    
risposta data 08.04.2018 - 00:17
fonte
0

Sì, hai torto. Il tuo metodo fa 2 cose diverse. Aggiunge foo a Foos, che cambia lo stato di db e salva tutto. Verificare che entrambe le cose accadano è valsa la pena. Il fatto che non ti piaccia tutto il lavoro sulle gambe per creare mock è un problema a parte. Potresti essere in grado di migliorare la testabilità passando un oggetto db / contesto / qualunque cosa.

    
risposta data 21.04.2018 - 20:43
fonte

Leggi altre domande sui tag