Perché ho bisogno di unit test per testare i metodi del repository?


19

Devo interpretare un po 'i diavoli sostenitori di questa domanda perché non posso difenderla bene a causa della mancanza di esperienza. Ecco l'accordo, ottengo concettualmente le differenze tra test unitari e test di integrazione. Quando si concentra specificamente sui metodi di persistenza e sul repository, un test unitario userebbe un mock possibilmente tramite un framework come Moq per affermare che un ordine cercato è stato restituito come previsto.

Diciamo che ho costruito il seguente test unitario:

[TestMethod]
public void GetOrderByIDTest()
{
   //Uses Moq for dependency for getting order to make sure 
   //ID I set up in 'Arrange' is same one returned to test in 'Assertion'
}

Quindi, se imposto OrderIdExpected = 5e il mio oggetto simulato ritorna 5come ID, il mio test passerà. Capisco. Ho testato il codice per accertarmi che le preforme del mio codice restituiscano l'oggetto e l'ID previsti e non qualcos'altro.

L'argomento che otterrò è questo:

"Perché non saltare semplicemente i test unitari ed eseguire i test di integrazione? È importante testare la procedura memorizzata nel database e il codice insieme. Sembra troppo lavoro extra per avere unit test e test di integrazione quando alla fine voglio sapere se il database chiama e il codice funziona. So che i test richiedono più tempo, ma devono essere eseguiti e testati indipendentemente, quindi mi sembra inutile avere entrambi. Basta testare contro ciò che conta. "

Potrei difenderlo con una definizione da manuale come: "Beh, questo è un test di integrazione e dobbiamo testare il codice separatamente come unit test e, yada, yada, yada ..." Questo è un caso in cui una spiegazione purista delle pratiche vs. la realtà si sta perdendo. Mi imbatto in questo a volte e se non riesco a difendere il ragionamento alla base del codice di unit test che alla fine si basa su dipendenze esterne, non riesco a darne ragione.

Qualsiasi aiuto su questa domanda è molto apprezzato, grazie!


2
dico inviarlo direttamente ai test degli utenti ... cambieranno comunque i requisiti ...
Nathan Hayfield

+1 per far
sì che

2
La semplice risposta è che ogni metodo può avere un numero x di casi limite (supponiamo che tu voglia testare ID positivi, ID di 0 e ID negativi). Se si desidera testare alcune funzionalità che utilizzano questo metodo, che a sua volta ha 3 casi limite, è necessario scrivere 9 casi test per testare ciascuna combinazione di casi limite. Isolandoli, devi solo scrivere 6. Inoltre, i test ti danno un'idea più specifica del perché qualcosa si è rotto. Forse il repository restituisce null in caso di errore e l'eccezione null viene generata diverse centinaia di righe nel codice.
Rob,

Penso che tu sia troppo severo nella definizione di un "test unitario". Cos'è una "unità di lavoro" per una classe di repository?
Caleb,

E assicurati di considerare questo: se il tuo test di unità prende in giro tutto ciò che stai provando a testare, cosa stai davvero testando?
Caleb,

Risposte:


20

I test unitari e i test di integrazione hanno scopi diversi.

I test unitari verificano la funzionalità del tuo codice ... che ottieni quello che ti aspetti dal metodo quando lo chiami. I test di integrazione testano il comportamento del codice quando combinato insieme come sistema. Non ti aspetteresti che i test unitari valutino il comportamento del sistema, né ti aspetteresti che i test di integrazione verifichino output specifici di un particolare metodo.

I test unitari, se eseguiti correttamente, sono più facili da configurare rispetto ai test di integrazione. Se fai affidamento esclusivamente ai test di integrazione, i tuoi test saranno:

  1. Sii più difficile da scrivere, nel complesso,
  2. Essere più fragili, a causa di tutte le dipendenze richieste, e
  3. Offri meno copertura del codice.

Bottom line: utilizzare i test di integrazione per verificare che le connessioni tra gli oggetti funzionino correttamente, ma basarsi innanzitutto sui test delle unità per verificare i requisiti funzionali.


Detto questo, potresti non aver bisogno di unit test per alcuni metodi di repository. I test unitari non dovrebbero essere eseguiti su metodi banali ; se tutto ciò che stai facendo è passare attraverso una richiesta di un oggetto al codice generato da ORM e restituire un risultato, non è necessario testare l'unità che, nella maggior parte dei casi; un test di integrazione è adeguato.


Sì, grazie per la rapida risposta, lo apprezzo. La mia unica critica alla risposta è che rientra nella preoccupazione del mio ultimo paragrafo. La mia opposizione ascolterà ancora spiegazioni e definizioni astratte e non qualche ragionamento più profondo. Ad esempio, potresti fornire il ragionamento applicato al mio caso d'uso di testare la stored procedure / DB in relazione al mio codice e perché i test unitari sono ancora preziosi in riferimento a questo particolare caso d'uso?
atconway

1
Se l'SP sta restituendo un risultato dal database, un test di integrazione potrebbe essere adeguato. Se contiene una logica non banale, vengono indicati i test unitari. Vedere anche stackoverflow.com/questions/1126614/… e msdn.microsoft.com/en-us/library/aa833169(v=vs.100).aspx (specifico di SQL Server).
Robert Harvey,

12

Sono dalla parte dei pragmatici. Non testare il tuo codice due volte.

Scriviamo solo test di integrazione per i nostri repository. Questi test dipendono solo da una semplice configurazione di test eseguita su un database in memoria. Penso che forniscano tutto ciò che un test unitario fa, e altro ancora.

  1. Possono sostituire i test unitari quando fanno TDD. Mentre c'è ancora un po 'di codice di prova per scrivere codice di prova prima di poter iniziare con il codice reale, funziona molto bene con l'approccio rosso / verde / refactor una volta che tutto è a posto.
  2. Testano il vero codice del repository, il codice contenuto nelle stringhe SQL o nei comandi ORM. È più importante verificare che la query sia corretta piuttosto che verificare che sia stata effettivamente inviata una stringa a un StatementExecutor.
  3. Sono eccellenti come test di regressione. Se falliscono, è sempre a causa di un problema reale, come una modifica dello schema che non è stata presa in considerazione.
  4. Sono indispensabili quando si modifica lo schema del database. Puoi essere sicuro di non aver modificato lo schema in modo da interrompere l'applicazione finché il test passa. (Il test unitario è inutile in questo caso, perché quando la procedura memorizzata non esiste più, il test unitario continuerà comunque.)

Davvero molto pragmatico. Ho riscontrato il caso che il database in memoria si comporti effettivamente diverso (non in termini di prestazioni) rispetto al mio database reale, a causa di un ORM leggermente diverso. Prenditi cura di questo nei test di integrazione.
Marcel,

1
@Marcel, mi sono imbattuto in quello. L'ho risolto a volte eseguendo tutti i miei test su un vero database.
Winston Ewert,

4

I test unitari forniscono un livello di modularità che i test di integrazione (in base alla progettazione) non possono. Quando un sistema è rifattorizzato o ricomposta, (e questo sarà accadere) unit test possono spesso essere riutilizzati, mentre i test di integrazione devono spesso essere riscritta. I test di integrazione che cercano di svolgere il lavoro di qualcosa che dovrebbe essere in un unit test spesso fanno troppo, il che li rende difficili da mantenere.

Inoltre, incluso il test unitario ha i seguenti vantaggi:

  1. I test unitari consentono di decomporre rapidamente un test di integrazione non riuscito (probabilmente a causa di un bug di regressione) e di identificare la causa. Inoltre, questo comunicherà il problema a tutto il team più rapidamente di diagrammi o altra documentazione.
  2. I test unitari possono servire come esempi e documentazione (un tipo di documentazione che effettivamente compila ) insieme al tuo codice. A differenza di altra documentazione, saprai immediatamente quando è obsoleta.
  3. I test unitari possono servire come indicatori delle prestazioni di base quando si cerca di scomporre problemi di prestazioni più grandi, mentre i test di integrazione tendono a richiedere molta strumentazione per trovare il problema.

È possibile dividere il lavoro (e applicare un approccio DRY) mentre si utilizzano sia i test Unit che Integration. È sufficiente fare affidamento su test unitari per unità di funzionalità di piccole dimensioni e non ripetere alcuna logica già presente in un test unitario in un test di integrazione. Ciò porterà spesso a meno lavoro (e quindi a meno rielaborazioni).


4

I test sono utili quando si interrompono: in modo proattivo o ri-attivo.

I test di unità sono proattivi e possono essere una verifica funzionale continua mentre i test di integrazione sono reattivi su dati in scena / falsi.

Se il sistema in esame è vicino / dipendente dai dati più che dalla logica di calcolo, i test di integrazione dovrebbero avere maggiore importanza.

Ad esempio, se stiamo costruendo un sistema ETL, i test più rilevanti riguarderanno i dati (messi in scena, falsi o in tempo reale). Lo stesso sistema avrà alcuni Test delle unità, ma solo intorno alla convalida, al filtro ecc.

Il modello di repository non dovrebbe avere logica computazionale o aziendale. È molto vicino al database. Ma il repository è anche vicino alla logica aziendale che lo utilizza. Quindi si tratta di colpire un SALDO.

I test unitari possono testare il comportamento del repository. I test di integrazione possono verificare ciò che è realmente accaduto quando è stato chiamato il repository.

Ora, "COSA È VERAMENTE SUCCESSO" potrebbe sembrare molto allettante. Ma questo potrebbe essere molto costoso per l'esecuzione costante e ripetuta. Qui si applica la definizione classica di test fragili.

Quindi la maggior parte delle volte, trovo abbastanza buono per scrivere Unit test su un oggetto che usa il repository. In questo modo testiamo se è stato chiamato il metodo di repository corretto e vengono restituiti risultati derisi.


Mi piace questo: "Ora," CHE COSA È VERAMENTE ACCADUTO "potrebbe sembrare molto allettante. Ma questo potrebbe essere molto costoso per correre in modo coerente e ripetuto".
atconway

2

Esistono 3 elementi separati che devono essere testati: la procedura memorizzata, il codice che chiama la procedura memorizzata (ovvero la classe del repository) e il consumatore. Il lavoro del repository è generare una query e convertire il set di dati restituito in un oggetto dominio. C'è abbastanza codice per supportare unit test che si separano dall'effettiva esecuzione della query e dalla creazione del set di dati.

Quindi (questo è un esempio molto molto semplificato):

interface IOrderRepository
{
    Order GetOrderByID(Guid id);
}

class OrderRepository : IOrderRepository
{
    private readonly ISqlExecutor sqlExecutor;
    public OrderRepository(ISqlExecutor sqlExecutor)
    {
        this.sqlExecutor = sqlExecutor;
    }

    public Order GetOrderByID(Guid id)
    {
        var sql = "SELECT blah, blah FROM Order WHERE OrderId = @p0";
        var dataset = this.sqlExecutor.Execute(sql, p0 = id);
        var result = this.orderFromDataset(dataset);
        return result;
    }
}

Quindi, durante il test OrderRepository, passare un oggetto deriso ISqlExecutore verificare che l'oggetto sottoposto a test passi nell'SQL corretto (che è il suo lavoro) e restituisca un Orderoggetto appropriato dati alcuni set di dati dei risultati (anch'essi derisi). Probabilmente l'unico modo per testare la SqlExecutorclasse concreta è con test di integrazione, abbastanza equi, ma questa è una classe wrapper sottile e raramente cambierà mai, quindi un grosso problema.

Devi comunque testare anche le tue procedure memorizzate.


0

Posso ridurlo a una linea di pensiero molto semplice: favorisci i test unitari perché aiutano a individuare i bug. E farlo costantemente perché le incoerenze causano confusione.

Ulteriori ragioni derivano dalle stesse regole che guidano lo sviluppo di OO poiché si applicano anche ai test. Ad esempio, il principio della responsabilità unica.

Se alcuni test unitari sembrano non valere la pena, allora forse è un segno che il soggetto non vale davvero la pena. O forse la sua funzionalità è più astratta della sua controparte e i test per essa (così come il suo codice) possono essere astratti ad un livello superiore.

Come con qualsiasi cosa, ci sono eccezioni. E la programmazione è ancora in qualche modo una forma d'arte, quindi molti problemi sono abbastanza diversi da giustificare la valutazione di ciascuno per l'approccio migliore.

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.