I test di integrazione (database) sono errati?

116

Alcune persone sostengono che i test di integrazione sono tutti i tipi di cattivo e sbagliato - tutto deve essere testato unitamente, il che significa che devi prendere in giro le dipendenze; un'opzione che, per vari motivi, non mi è sempre piaciuta.

Trovo che, in alcuni casi, un test unitario semplicemente non provi nulla.

Prendiamo come esempio la seguente (banale, ingenua) implementazione del repository (in PHP):

class ProductRepository
{
    private $db;

    public function __construct(ConnectionInterface $db) {
        $this->db = $db;
    }

    public function findByKeyword($keyword) {
        // this might have a query builder, keyword processing, etc. - this is
        // a totally naive example just to illustrate the DB dependency, mkay?

        return $this->db->fetch("SELECT * FROM products p"
            . " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
    }
}

Diciamo che voglio provare in un test che questo repository può effettivamente trovare prodotti che corrispondono a varie parole chiave date.

A corto di test di integrazione con un oggetto di connessione reale, come faccio a sapere che questo sta effettivamente generando query reali e che quelle query fanno effettivamente quello che penso che facciano?

Se devo prendere in giro l'oggetto di connessione in un test di unità, posso solo provare cose come "genera la query prevista", ma ciò non significa che in realtà lavori . .. cioè, forse sta generando la query che mi aspettavo, ma forse quella query non fa quello che penso che faccia.

In altre parole, mi sento come un test che fa affermazioni sulla query generata, è essenzialmente senza valore, perché sta testando come il metodo findByKeyword() sia implementato , ma ciò non prova che in realtà funziona .

Questo problema non si limita ai repository o all'integrazione del database - sembra applicarsi in molti casi, dove fare asserzioni sull'uso di un mock (test-double) dimostra solo come sono implementate le cose, non se sono " sta andando davvero a lavorare.

Come gestisci situazioni come queste?

I test di integrazione sono davvero "cattivi" in un caso come questo?

Capisco che è meglio testare una cosa, e capisco anche perché i test di integrazione conducano a una miriade di percorsi di codice, che non possono essere testati, ma nel caso di un servizio (come un repository) di cui l'unico scopo è interagire con un altro componente, come si può realmente testare qualcosa senza test di integrazione?

    
posta mindplay.dk 02.11.2015 - 17:58
fonte

11 risposte

123

Il tuo collaboratore ha ragione nel ritenere che tutto ciò che può essere testato unitamente dovrebbe essere testato unitamente, e hai ragione che i test unitari ti porteranno solo fino a così lontano e non oltre, soprattutto quando scrivi semplici wrapper attorno a complessi servizi esterni.

Un modo comune di pensare al testing è come una piramide di test . È un concetto spesso connesso con Agile e molti hanno scritto a riguardo, tra cui Martin Fowler (che lo attribuisce a Mike Cohn in Riuscita con Agile ), Alistair Scott e Blog di test di Google .

        /\                           --------------
       /  \        UI / End-to-End    \          /
      /----\                           \--------/
     /      \     Integration/System    \      /
    /--------\                           \----/
   /          \          Unit             \  /
  --------------                           \/
  Pyramid (good)                   Ice cream cone (bad)

L'idea è che i test unitari rapidi e resistenti sono alla base del processo di test: dovrebbero esserci test unitari più mirati rispetto ai test di sistema / integrazione e più test di sistema / integrazione rispetto ai test end-to-end. Man mano che ci si avvicina alla cima, i test tendono a richiedere più tempo / risorse per essere eseguiti, tendono ad essere soggetti a più fragilità e sfaldamento e sono meno specifici nell'individuare quale sistema o file è rotto ; naturalmente, è preferibile evitare di essere "top-pesanti".

A questo punto, i test di integrazione non sono cattivi , ma un strong affidamento su di essi potrebbe indicare che non sono stati progettati i singoli componenti per essere facili da testare. Ricorda, l'obiettivo qui è verificare che la tua unità stia funzionando in base alle sue specifiche, mentre coinvolge un minimo di altri sistemi distruttibili : potresti provare un database in memoria (che conto come unità) test di prova doppio insieme a simulazioni) per il test di bordo pesante, ad esempio, e quindi scrivere un paio di test di integrazione con il vero motore di database per stabilire che i casi principali funzionano quando il sistema è assemblato.

Come nota a margine, hai menzionato che le scritte che scrivi scrivono semplicemente come viene implementata qualcosa, non se funziona . È una specie di antipattern: un test che è uno specchio perfetto della sua implementazione non sta davvero testando nulla. Invece, prova che ogni classe o metodo si comporta secondo le sue specifiche , a qualsiasi livello di astrazione o realismo che richiede.

    
risposta data 02.11.2015 - 20:39
fonte
87

One of my co-workers maintains that integration tests are all kinds of bad and wrong - everything must be unit-tested,

È un po 'come dire che gli antibiotici sono cattivi - tutto dovrebbe essere curato con vitamine.

Test di unità non possono catturare tutto - verificano solo come un componente funziona in un ambiente controllato . I test di integrazione verificano che tutto funzioni insieme , che è più difficile da fare ma più significativo alla fine.

Un processo di test valido e completo utilizza entrambi tipi di test: test unitari per verificare le regole aziendali e altre cose che possono essere testate in modo indipendente e test di integrazione per assicurarsi che tutto funzioni insieme.

Short of integration testing with a real connection object, how can I know that this is actually generating real queries - and that those queries actually do what I think they do?

potresti unità testarlo a livello di database . Esegui la query con vari parametri e vedi se ottieni i risultati che ti aspetti. Concesso significa copiare / incollare eventuali modifiche nel codice "vero". ma fa ti permette di testare la query indipendentemente da qualsiasi altra dipendenza.

    
risposta data 02.11.2015 - 18:03
fonte
15

I test unitari non catturano tutti i difetti. Ma sono meno costosi da configurare e (ri) eseguire rispetto ad altri tipi di test. I test unitari sono giustificati dalla combinazione di un valore moderato e un costo da basso a moderato.

Ecco una tabella che mostra i tassi di rilevamento dei difetti per diversi tipi di test.

fonte: p.470 in codice completo 2 di McConnell

    
risposta data 03.11.2015 - 05:23
fonte
12

No, non sono male. Si spera, si dovrebbero avere test di unità e di integrazione. Vengono utilizzati e eseguiti in diverse fasi del ciclo di sviluppo.

Test delle unità

I test delle unità dovrebbero essere eseguiti sul server di build e localmente, dopo che il codice è stato compilato. Se i test di un'unità falliscono, si dovrebbe fallire la compilazione o non eseguire il commit dell'aggiornamento del codice fino a quando i test non saranno risolti. Il motivo per cui vogliamo che i test unitari siano isolati è che vogliamo che il server di compilazione sia in grado di eseguire tutti i test senza tutte le dipendenze. Quindi potremmo eseguire la build senza tutte le dipendenze complesse richieste e avere molti test che funzionano molto velocemente.

Quindi, per un database, si dovrebbe avere qualcosa del tipo:

IRespository

List<Product> GetProducts<String Size, String Color);

Ora la vera implementazione di IRepository andrà al database per ottenere i prodotti, ma per il test delle unità, si può prendere in giro IRepository con uno falso per eseguire tutti i test necessari senza un database di actaul in quanto possiamo simulare tutti i tipi di liste di prodotti restituiti dall'istanza di simulazione e testare qualsiasi logica aziendale con i dati derisi.

Test di integrazione

I test di integrazione sono in genere test di superamento dei limiti. Vogliamo eseguire questi test sul server di distribuzione (l'ambiente reale), sulla sandbox o anche localmente (indicato come sandbox). Non vengono eseguiti sul server di build. Dopo che il software è stato distribuito in ambiente, in genere questi verranno eseguiti come attività di post-distribuzione. Possono essere automatizzati tramite le utilità della riga di comando. Ad esempio, possiamo eseguire nUnit dalla riga di comando se categorizziamo tutto il test di integrazione che vogliamo richiamare. Questi in realtà chiamano il vero repository con la vera chiamata al database. Questo tipo di test aiuta con:

  • Disponibilità della stabilità sanitaria ambientale
  • Test della cosa reale

Questi test sono a volte più difficili da eseguire in quanto potrebbe essere necessario impostare e / o abbattere. Prendi in considerazione l'aggiunta di un prodotto. Probabilmente vorremmo aggiungere il prodotto, interrogarlo per vedere se è stato aggiunto, e dopo averlo fatto, rimuoverlo. Non vogliamo aggiungere 100 o 1000 di prodotti di "integrazione", quindi è necessario un ulteriore set-up.

I test di integrazione possono rivelarsi preziosi per convalidare un ambiente e accertarsi che la cosa reale funzioni.

Uno dovrebbe avere entrambi.

  • Esegui i test unitari per ogni build.
  • Esegui i test di integrazione per ogni distribuzione.
risposta data 02.11.2015 - 22:13
fonte
11

I test di integrazione del database non sono male. Ancor più, sono necessari.

Probabilmente hai diviso la tua applicazione in livelli, ed è una buona cosa. Puoi testare ogni livello isolandolo deridendo i livelli vicini, e questa è una buona cosa. Ma non importa quanti livelli di astrazione crei, a un certo punto ci deve essere un livello che fa il lavoro sporco - in realtà parla al database. A meno che tu non lo collaudi, prova non affatto. Se esegui il test del livello n con il mocking layer n-1 stai valutando l'ipotesi che il livello n funzioni a condizione che strato n-1 funziona. Affinché questo funzioni, devi in qualche modo dimostrare che il livello 0 funziona.

Sebbene in teoria si potesse unificare il database di test, analizzando e interpretando l'SQL generato, è molto più facile e più affidabile creare database di test al volo e parlarne con esso.

Conclusione

Qual è la confidenza generata dal test dell'unità Abstract Repository , Ethereal Object-Relational-Mapper , Generic Active Record , Teorico Livelli di persistenza , quando alla fine il codice SQL generato contiene errore di sintassi?

    
risposta data 02.11.2015 - 23:14
fonte
6

Hai bisogno di entrambi.

Nel tuo esempio se stavi testando che un database in una determinata condizione, quando viene eseguito il metodo findByKeyword , recuperi i dati, ti aspetti che questo sia un buon test di integrazione.

In qualsiasi altro codice che utilizza quel metodo findByKeyword devi controllare cosa viene inserito nel test, in modo da poter restituire null o le parole giuste per il test o qualsiasi altra cosa, quindi prendi in giro la dipendenza del database in modo da sapere esattamente cosa riceverà il test (e si perde il sovraccarico di connettersi a un database e assicurarsi che i dati siano corretti)

    
risposta data 02.11.2015 - 18:07
fonte
6

L'autore del articolo del blog a cui fai riferimento è riguarda principalmente la potenziale complessità che può derivare dai test integrati (sebbene sia scritta in modo molto ponderato e categorico). Tuttavia, i test integrati non sono necessariamente negativi, e alcuni sono in realtà più utili dei puri test unitari. Dipende davvero dal contesto della tua applicazione e da cosa stai provando a testare.

Molte applicazioni oggi semplicemente non funzionerebbero affatto se il loro server database fallisse. Almeno, pensaci nel contesto della funzione che stai provando a testare.

Da un lato, se quello che stai provando a testare non dipende, o può essere fatto per non dipendere affatto, dal database, quindi scrivi il test in modo tale da non provare nemmeno utilizzare il database (basta fornire dati fittizi come richiesto).  Ad esempio, se stai provando a testare alcune logiche di autenticazione quando servi una pagina web (per esempio), è probabilmente una buona cosa staccarlo del tutto dal DB (supponendo che tu non faccia affidamento sul DB per l'autenticazione, o che puoi prenderlo in giro abbastanza facilmente).

D'altra parte, se si tratta di una funzionalità che si basa direttamente sul database e che non funzionerebbe in un ambiente reale, il database non sarebbe disponibile, quindi si deriderà cosa fa il DB nel codice client DB (ad es. strato usando quel DB) non ha necessariamente senso.

Ad esempio, se si è certi che la propria applicazione si baserà su un database (e possibilmente su un sistema di database specifico), deridere il comportamento del database per il suo scopo spesso sarà una perdita di tempo. I motori di database (in particolare RDBMS) sono sistemi complessi. Alcune righe di SQL possono effettivamente svolgere molto lavoro, che sarebbe difficile da simulare (in effetti, se la tua query SQL è lunga alcune righe, è probabile che avrai bisogno di molte più linee di Java / PHP / C # / Python codice per produrre internamente lo stesso risultato): duplicare la logica che hai già implementato nel DB non ha senso e controllare che il codice di test diventi quindi un problema di per sé.

Non lo tratterei necessariamente come un problema di unit test v.s. test integrato , ma piuttosto guarda lo scopo di ciò che viene testato. Rimangono i problemi generali dei test di unità e integrazione: è necessario un insieme ragionevolmente realistico di dati di test e casi di test, ma anche qualcosa che sia sufficientemente piccolo perché i test possano essere eseguiti rapidamente.

Il tempo di reimpostare il database e ripopolare con i dati di test è un aspetto da considerare; generalmente lo valuterai in base al tempo impiegato per scrivere quel codice simulato (che dovresti mantenere anche tu, eventualmente).

Un altro punto da considerare è il grado di dipendenza che l'applicazione ha con il database.

  • Se la tua applicazione segue semplicemente un modello CRUD, in cui hai un livello di astrazione che ti consente di scambiare tra qualsiasi RDBMS con il semplice metodo di configurazione, è probabile che sarai in grado di lavorare con un sistema di simulazione abbastanza facilmente (probabilmente confondendo la linea tra unità e test integrati usando un RDBMS in memoria).
  • Se la tua applicazione utilizza una logica più complessa, qualcosa che sarebbe specifico per uno di SQL Server, MySQL, PostgreSQL (per esempio), allora in genere avrebbe più senso avere un test che usi quel sistema specifico.
risposta data 03.11.2015 - 15:04
fonte
1

Hai ragione a pensare a un test unitario tanto incompleto. L'incompletezza è nell'interfaccia del database che viene derisa. L'aspettativa o le asserzioni di quell'ingenua finta sono incomplete.

Per renderlo completo, dovresti risparmiare tempo e risorse sufficienti per scrivere o integrare un motore di regole SQL che garantire che l'istruzione SQL emessa dall'oggetto sottoposto a test, porterebbe a previsioni operazioni.

Tuttavia, l'alternativa spesso dimenticata e alquanto costosa / compagno di derisione è "virtualizzazione" .

È possibile creare un'istanza DB temporanea, in memoria ma "reale" per testare una singola funzione? sì ? lì, hai un test migliore, quello che controlla i dati reali salvati e recuperati.

Ora, si potrebbe dire, hai trasformato un test unitario in un test di integrazione. Ci sono punti di vista diversi su dove tracciare la linea per classificare tra test unitari e test di integrazione. IMHO, "unità" è una definizione arbitraria e dovrebbe soddisfare le tue esigenze.

    
risposta data 03.11.2015 - 21:15
fonte
0

Unit Tests e Integration Tests sono ortogonali tra loro. Offrono una vista diversa sull'applicazione che stai creando. Di solito vuoi entrambi . Ma il punto nel tempo differisce, quando vuoi quel tipo di test.

Il più spesso vuoi Unit Tests . I test unitari si concentrano su una piccola parte del codice sottoposto a test: ciò che viene chiamato esattamente unit è lasciato al lettore. Ma lo scopo è semplice: ottenere un veloce feedback di quando e dove il tuo codice ha rotto . Detto questo, dovrebbe essere chiaro, che le chiamate a un DB effettivo sono nono .

D'altra parte, ci sono cose che possono essere unità testate in condizioni difficili senza un database. Forse c'è una condizione di razza nel tuo codice e una chiamata a un DB genera una violazione di unique constraint che potrebbe essere generata solo se effettivamente usi il tuo sistema. Ma quei tipi di test sono costosi non puoi (e non vuoi) eseguirli tutte le volte che unit tests .

    
risposta data 27.04.2016 - 20:01
fonte
0

Nel mondo .Net ho l'abitudine di creare un progetto di test e di creare test come metodo di codifica / debug / test di andata e ritorno meno l'interfaccia utente. Questo è un modo efficace per me di svilupparsi. Non ero interessato a eseguire tutti i test per ogni build (perché rallenta il flusso di lavoro di sviluppo), ma capisco l'utilità di questo per un team più grande. Tuttavia, è possibile stabilire una regola che prima di eseguire il codice, tutti i test devono essere eseguiti e superati (se il test richiede più tempo perché il database viene effettivamente colpito).

Deridere il livello di accesso ai dati (DAO) e non effettivamente colpire il database, non solo non mi consente di codificare il modo in cui mi piace e mi sono abituato, ma manca una parte importante della base di codice reale. Se non stai veramente testando il livello di accesso ai dati e il database e fai solo finta, e poi passi un sacco di tempo a prendere in giro le cose, non riesco a cogliere l'utilità di questo approccio per testare realmente il mio codice. Sto testando un piccolo pezzo invece di uno più grande con un test. Capisco che il mio approccio potrebbe essere più simile a un test di integrazione, ma sembra che il test unitario con la simulazione sia una perdita di tempo ridondante se in realtà si scrive il test di integrazione solo una volta. È anche un buon modo per sviluppare ed eseguire il debug.

In effetti, per un po 'di tempo sono stato a conoscenza di TDD e Behavior Driven Design (BDD) e ho pensato ai modi per usarlo, ma è difficile aggiungere retroattivamente i test unitari. Forse mi sbaglio, ma scrivere un test che copra più codice end-to-end con il database incluso, sembra un test molto più completo e con priorità più alta da scrivere che copre più codice ed è un modo più efficiente di scrivere test.

In effetti, penso che qualcosa come Behaviour Design (BDD) che tenta di testare end-to-end con il linguaggio specifico del dominio (DSL) dovrebbe essere la strada da percorrere. Abbiamo SpecFlow nel mondo .Net, ma è iniziato come open source con Cucumber.

link

Non sono davvero impressionato dalla vera utilità del test che ho scritto prendendo in giro il livello di accesso ai dati e non colpendo il database. L'oggetto restituito non ha colpito il database e non è stato popolato con i dati. Era un oggetto completamente vuoto che dovevo prendere in giro in modo innaturale. Penso solo che sia una perdita di tempo.

Secondo Stack Overflow, il mocking viene usato quando gli oggetti reali sono poco pratici da incorporare nel test dell'unità.

link

"Il mocking viene utilizzato principalmente nei test di unità: un oggetto sottoposto a test può avere dipendenze da altri oggetti (complessi). Per isolare il comportamento dell'oggetto che vuoi testare, sostituisci gli altri oggetti con mock che simulano il comportamento del oggetti reali Questo è utile se gli oggetti reali sono poco pratici da incorporare nel test unitario. "

La mia tesi è che se sto codificando qualcosa end-to-end (interfaccia utente Web a livello aziendale a livello accesso dati a database, andata e ritorno), prima di controllare qualsiasi cosa come sviluppatore, vado a testare questo round trip flusso. Se ritocco l'interfaccia utente e eseguo il debug e testare questo flusso a partire da un test, sto testando tutto a breve dell'interfaccia utente e restituendo esattamente ciò che l'UI si aspetta. Tutto quello che mi rimane è di inviare all'interfaccia utente quello che vuole.

Ho un test più completo che fa parte del mio flusso di lavoro di sviluppo naturale. Per me, questo dovrebbe essere il test con la priorità più alta che copra il più possibile le specifiche dell'utente finale. Se non creo mai altri test più granulari, almeno ho questo test più completo che dimostra che la mia funzionalità desiderata funziona.

Un co-fondatore di Stack Exchange non è convinto dei benefici di avere una copertura del 100% del test unitario. Anche io non lo sono. Vorrei fare un "test di integrazione" più completo che colpisca il database mantenendo un sacco di mock di database ogni giorno.

link

    
risposta data 09.03.2017 - 19:29
fonte
-1

Le dipendenze esterne dovrebbero essere prese in giro perché non puoi controllarle (potrebbero passare durante la fase di test di integrazione ma fallire nella produzione). Le unità possono fallire, le connessioni al database possono fallire per un numero qualsiasi di motivi, potrebbero esserci problemi di rete, ecc. Avere test di integrazione non dà ulteriore sicurezza perché sono tutti problemi che potrebbero verificarsi in fase di runtime.

Con test di unità reali, stai testando entro i limiti della sandbox e dovrebbe essere chiaro. Se uno sviluppatore ha scritto una query SQL che ha fallito in QA / PROD significa che non l'ha nemmeno provato una volta prima di quel momento.

    
risposta data 02.11.2015 - 20:26
fonte