Qual è la migliore strategia per le applicazioni basate su database di test unitari?


346

Lavoro con molte applicazioni web guidate da database di varia complessità sul back-end. In genere, esiste un livello ORM separato dalla logica aziendale e di presentazione. Ciò rende il test unitario della logica di business abbastanza semplice; le cose possono essere implementate in moduli discreti e tutti i dati necessari per il test possono essere falsificati attraverso il derisione di oggetti.

Ma testare l'ORM e il database stesso è sempre stato pieno di problemi e compromessi.

Nel corso degli anni ho provato alcune strategie, nessuna delle quali mi ha completamente soddisfatto.

  • Carica un database di test con dati noti. Esegui test con l'ORM e verifica che i dati corretti vengano restituiti. Lo svantaggio qui è che il tuo DB di prova deve tenere il passo con qualsiasi modifica dello schema nel database dell'applicazione e potrebbe non essere sincronizzato. Si basa anche su dati artificiali e potrebbe non esporre bug che si verificano a causa di input stupidi da parte dell'utente. Infine, se il database di test è piccolo, non rivelerà inefficienze come un indice mancante. (OK, l'ultimo non è proprio quello per cui i test unitari dovrebbero essere usati, ma non fa male.)

  • Caricare una copia del database di produzione e testarlo. Il problema qui è che potresti non avere idea di cosa ci sia nel DB di produzione in un dato momento; potrebbe essere necessario riscrivere i test se i dati cambiano nel tempo.

Alcune persone hanno sottolineato che entrambe queste strategie si basano su dati specifici e un test unitario dovrebbe testare solo la funzionalità. A tal fine, ho visto suggerito:

  • Utilizzare un server database fittizio e verificare solo che l'ORM stia inviando le query corrette in risposta a una determinata chiamata del metodo.

Quali strategie hai utilizzato per testare le applicazioni basate su database, se ce ne sono? Cosa ha funzionato meglio per te?


Penso che dovresti avere ancora indici di database in un ambiente di test per casi come indici univoci.
dtc,

Non mi interessa questa domanda qui, ma se rispettiamo le regole, questa domanda non è per StackOverflow ma piuttosto per il sito Web softwareengineering.stackexchange .
ITExpert il

Risposte:


155

In realtà ho usato il tuo primo approccio con un discreto successo, ma in un modo leggermente diverso che penso risolverà alcuni dei tuoi problemi:

  1. Conservare l'intero schema e gli script per crearlo nel controllo del codice sorgente in modo che chiunque possa creare lo schema del database corrente dopo un check out. Inoltre, conserva i dati di esempio nei file di dati che vengono caricati durante una parte del processo di generazione. Quando scopri i dati che causano errori, aggiungili ai dati di esempio per verificare che gli errori non riemergano.

  2. Utilizzare un server di integrazione continua per creare lo schema del database, caricare i dati di esempio ed eseguire i test. In questo modo manteniamo sincronizzato il nostro database di test (ricostruendolo ad ogni esecuzione di test). Sebbene ciò richieda che il server CI abbia accesso e proprietà della propria istanza di database dedicata, dico che avere il nostro schema db creato 3 volte al giorno ha notevolmente aiutato a trovare errori che probabilmente non sarebbero stati trovati fino a poco prima della consegna (se non dopo ). Non posso dire di aver ricostruito lo schema prima di ogni commit. Qualcuno? Con questo approccio non dovrai farlo (beh forse dovremmo, ma non è un grosso problema se qualcuno dimentica).

  3. Per il mio gruppo, l'input dell'utente viene eseguito a livello di applicazione (non db), quindi questo viene testato tramite test unitari standard.

Caricamento della copia del database di produzione:
questo era l'approccio utilizzato nel mio ultimo lavoro. È stata un'enorme causa di dolore di un paio di problemi:

  1. La copia sarebbe scaduta dalla versione di produzione
  2. Le modifiche verrebbero apportate allo schema della copia e non verrebbero propagate ai sistemi di produzione. A questo punto avremmo schemi divergenti. Non è divertente.

Server di database beffardo: lo
facciamo anche nel mio lavoro attuale. Dopo ogni commit eseguiamo unit test contro il codice dell'applicazione a cui sono stati iniettati finti accessori db. Quindi tre volte al giorno eseguiamo la build completa di db sopra descritta. Consiglio vivamente entrambi gli approcci.


37
Il caricamento di una copia del database di produzione ha anche implicazioni per la sicurezza e la privacy. Una volta che diventa grande, prenderne una copia e metterlo nel tuo ambiente di sviluppo può essere un grosso problema.
WW.

onestamente, questo è un dolore enorme. Sono nuovo ai test e ho anche scritto un orm che voglio testare. Ho già usato il tuo primo metodo, ma leggi che non crea l'unità di test. Uso specifiche funzionalità del motore db e quindi prendere in giro un DAO sarà difficile. Penso che semplicemente utilizzerò il mio metodo attuale poiché funziona e altri lo usano. Prove automatizzate rock btw. Grazie.
gelido

2
Gestisco due diversi progetti di grandi dimensioni, in uno di questi questo approccio è stato perfetto, ma abbiamo avuto molti problemi cercando di implementare questo è nell'altro progetto. Quindi penso che dipende da quanto facilmente possa essere ricreato lo schema ogni volta per eseguire i test, attualmente sto lavorando per trovare una nuova soluzione per questo sempre ultimo problema.
Croce

2
In questo caso, vale sicuramente la pena utilizzare uno strumento di controllo delle versioni del database come Roundhouse, qualcosa che può eseguire le migrazioni. Questo può essere eseguito su qualsiasi istanza DB e dovrebbe assicurarsi che gli schemi siano aggiornati. Inoltre, quando vengono scritti gli script di migrazione, devono essere scritti anche i dati dei test, mantenendo sincronizzati migrazioni e dati.
jedd.ahyoung,

meglio usare patch e beffe di scimmie ed evitare operazioni di scrittura
Nickpick,

56

Eseguo sempre test su un DB in memoria (HSQLDB o Derby) per questi motivi:

  • Ti fa pensare quali dati conservare nel tuo DB di test e perché. Trasportare il proprio DB di produzione in un sistema di test si traduce in "Non ho idea di cosa sto facendo o perché e se qualcosa si rompe, non sono stato io !!" ;)
  • Si assicura che il database possa essere ricreato con poco sforzo in una nuova posizione (ad esempio quando è necessario replicare un bug dalla produzione)
  • Aiuta enormemente con la qualità dei file DDL.

Il DB in memoria viene caricato con nuovi dati una volta avviati i test e dopo la maggior parte dei test, invoco ROLLBACK per mantenerlo stabile. Mantenere SEMPRE stabili i dati nel DB di test! Se i dati cambiano continuamente, non è possibile eseguire il test.

I dati vengono caricati da SQL, da un DB modello o da un dump / backup. Preferisco i dump se sono in un formato leggibile perché posso metterli in VCS. Se ciò non funziona, utilizzo un file CSV o XML. Se devo caricare enormi quantità di dati ... Non lo faccio. Non devi mai caricare enormi quantità di dati :) Non per test unitari. I test delle prestazioni sono un altro problema e si applicano regole diverse.


1
La velocità è l'unica ragione per usare (specificamente) un DB in memoria?
Rinogo,

2
Immagino che un altro vantaggio potrebbe essere la sua natura "usa e getta" - non c'è bisogno di ripulire dopo te stesso; basta uccidere il DB in memoria. (Ma ci sono altri modi per ottenere questo risultato, come l'approccio ROLLBACK che hai citato)
rinogo

1
Il vantaggio è che ogni test può scegliere la propria strategia individualmente. Abbiamo test che fanno il lavoro nei thread figlio, il che significa che Spring impegnerà sempre i dati.
Aaron Digulla,

@Aaron: stiamo seguendo anche questa strategia. Vorrei sapere qual è la tua strategia per affermare che il modello in memoria ha la stessa struttura del vero db?
Guillaume,

1
@Guillaume: sto creando tutti i database dagli stessi file SQL. H2 è ottimo per questo poiché supporta la maggior parte delle idiosincrasie SQL dei principali database. Se non funziona, utilizzo un filtro che accetta l'SQL originale e lo converte in SQL per il database in memoria.
Aaron Digulla,

14

Faccio questa domanda da molto tempo, ma penso che non ci siano proiettili d'argento per questo.

Quello che attualmente faccio è deridere gli oggetti DAO e mantenere una rappresentazione in memoria di una buona raccolta di oggetti che rappresentano casi interessanti di dati che potrebbero vivere nel database.

Il problema principale che vedo con questo approccio è che stai coprendo solo il codice che interagisce con il tuo livello DAO, ma non testando mai il DAO stesso, e nella mia esperienza vedo che si verificano molti errori anche su quel livello. Tengo anche alcuni test unitari eseguiti sul database (per il gusto di utilizzare TDD o test rapidi localmente), ma quei test non vengono mai eseguiti sul mio server di integrazione continua, poiché non conserviamo un database a tale scopo e io pensare che i test eseguiti sul server CI dovrebbero essere autonomi.

Un altro approccio che trovo molto interessante, ma che non vale sempre la pena poiché richiede un po 'di tempo, è quello di creare lo stesso schema utilizzato per la produzione su un database incorporato che viene semplicemente eseguito all'interno dell'unità di test.

Anche se non c'è dubbio che questo approccio migliora la tua copertura, ci sono alcuni svantaggi, dal momento che devi essere il più vicino possibile a ANSI SQL per farlo funzionare sia con il tuo DBMS corrente che con la sostituzione integrata.

Indipendentemente da cosa pensi sia più rilevante per il tuo codice, ci sono alcuni progetti là fuori che possono renderlo più semplice, come DbUnit .


13

Anche se ci sono strumenti che permettono di deridere il database in un modo o nell'altro (ad esempio jOOQ 's MockConnection, che può essere visto in questa risposta - disclaimer, io lavoro per il fornitore di jOOQ), vorrei consigliare non prendere in giro i database più grandi con complessi interrogazioni.

Anche se desideri solo testare l'integrazione del tuo ORM, fai attenzione che un ORM invia una serie molto complessa di query al tuo database, che può variare in

  • sintassi
  • complessità
  • ordine (!)

Deridere tutto ciò per produrre dati fittizi sensibili è piuttosto difficile, a meno che non si stia effettivamente costruendo un piccolo database all'interno del proprio finto, che interpreta le istruzioni SQL trasmesse. Detto questo, utilizzare un noto database di test di integrazione che è possibile ripristinare facilmente con dati noti, rispetto al quale è possibile eseguire i test di integrazione.


5

Io uso il primo (eseguendo il codice su un database di test). L'unico problema sostanziale che vedo sollevare con questo approccio è la possibilità che gli schemi vadano fuori sincrono, di cui mi occupo mantenendo un numero di versione nel mio database e apportando tutte le modifiche allo schema tramite uno script che applica le modifiche per ogni incremento di versione.

Apporto inoltre tutte le modifiche (incluso lo schema del database) rispetto al mio ambiente di test, quindi alla fine è il contrario: dopo aver superato tutti i test, applica gli aggiornamenti dello schema all'host di produzione. Mantengo anche una coppia separata di database di test e applicazioni sul mio sistema di sviluppo in modo da poter verificare lì che l'aggiornamento del db funzioni correttamente prima di toccare le caselle di produzione reali.


3

Sto usando il primo approccio, ma un po 'diverso che consente di affrontare i problemi che hai citato.

Tutto il necessario per eseguire test per DAO è nel controllo del codice sorgente. Include schema e script per creare il DB (la finestra mobile è ottima per questo). Se è possibile utilizzare il DB incorporato, lo utilizzo per la velocità.

La differenza importante con gli altri approcci descritti è che i dati richiesti per il test non vengono caricati da script SQL o file XML. Tutto (tranne alcuni dati del dizionario che sono effettivamente costanti) viene creato dall'applicazione utilizzando funzioni / classi di utilità.

Lo scopo principale è quello di rendere i dati utilizzati dal test

  1. molto vicino al test
  2. esplicito (l'utilizzo di file SQL per i dati rende molto problematico vedere quale dato viene utilizzato da quale test)
  3. isolare i test dalle modifiche non correlate.

Fondamentalmente significa che queste utilità consentono di specificare in modo dichiarativo solo le cose essenziali per il test nel test stesso e di omettere le cose irrilevanti.

Per dare un'idea di cosa significhi in pratica, considerare il test per qualche DAO che funziona con Comments a Posts scritto da Authors. Al fine di testare le operazioni CRUD per tale DAO alcuni dati dovrebbero essere creati nel DB. Il test sarebbe simile a:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Ciò presenta numerosi vantaggi rispetto agli script SQL o ai file XML con dati di test:

  1. Mantenere il codice è molto più semplice (l'aggiunta di una colonna obbligatoria, ad esempio in alcune entità a cui fa riferimento molti test, come Autore, non richiede la modifica di molti file / record, ma solo una modifica nel builder e / o nella factory)
  2. I dati richiesti dal test specifico sono descritti nel test stesso e non in qualche altro file. Questa vicinanza è molto importante per la comprensibilità del test.

Rollback vs Commit

Trovo più conveniente che i test eseguano il commit quando vengono eseguiti. Innanzitutto, alcuni effetti (ad esempio DEFERRED CONSTRAINTS) non possono essere controllati se il commit non si verifica mai. In secondo luogo, quando un test fallisce, i dati possono essere esaminati nel DB in quanto non vengono ripristinati dal rollback.

Di causa questo ha un aspetto negativo che il test può produrre dati non funzionanti e questo porterà a fallimenti in altri test. Per far fronte a questo, provo a isolare i test. Nell'esempio sopra, ogni test può creare nuove Authore tutte le altre entità vengono create correlate ad esso, quindi le collisioni sono rare. Per gestire le rimanenti invarianti che possono essere potenzialmente infrante ma non possono essere espresse come un vincolo a livello di DB, utilizzo alcuni controlli programmatici per condizioni errate che possono essere eseguite dopo ogni singolo test (e sono eseguite in CI ma di solito sono disattivate localmente per le prestazioni motivi).


Se si esegue il seeding del database utilizzando entità e gli script orm anziché sql, ha anche il vantaggio che il compilatore ti costringerà a correggere il codice seed se si apportano modifiche al modello. Rilevante solo se si utilizza un linguaggio tipizzato statico, ovviamente.
daramasala,

Quindi per chiarimenti: stai usando le funzioni / le classi di utilità in tutta la tua applicazione o solo per i tuoi test?
Ella

@Ella queste funzioni di utilità di solito non sono necessarie al di fuori del codice di test. Pensa ad esempio a PostBuilder.post(). Genera alcuni valori per tutti gli attributi obbligatori del post. Questo non è necessario nel codice di produzione.
Roman Konoval,

2

Per progetti basati su JDBC (direttamente o indirettamente, ad es. JPA, EJB, ...) è possibile simulare non l'intero database (in tal caso sarebbe meglio usare un db di prova su un RDBMS reale), ma solo un mockup a livello JDBC .

Il vantaggio è l'astrazione che ne deriva, dato che i dati JDBC (set di risultati, conteggio aggiornamenti, avviso, ...) sono gli stessi qualunque sia il backend: il tuo db di produzione, un db di prova o solo alcuni dati di simulazione forniti per ogni prova Astuccio.

Con la connessione JDBC simulata per ogni caso non è necessario gestire il test db (pulizia, solo un test alla volta, ricaricare i dispositivi, ...). Ogni connessione del mockup è isolata e non è necessario ripulirla. In ogni caso di test vengono forniti solo dispositivi minimi necessari per simulare lo scambio JDBC, che aiuta a evitare la complessità della gestione di un intero db di test.

Acolyte è il mio framework che include un driver JDBC e utility per questo tipo di modello: http://acolyte.eu.org .

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.