Test unitari e database: a che punto mi collego effettivamente al database?


37

Ci sono risposte alla domanda su come le classi di test che si collegano a un database, ad esempio "Le classi di test di servizio devono connettersi ..." e "Test unità - App accoppiata al database" .

Quindi, in breve, supponiamo di avere una classe A che deve connettersi a un database. Invece di consentire a A di connettersi effettivamente, si fornisce A con un'interfaccia che A può utilizzare per connettersi. Per i test si implementa questa interfaccia con alcune cose - ovviamente senza connettersi. Se la classe B crea un'istanza A, deve passare una connessione di database "reale" ad A. Ma ciò significa che B apre una connessione di database. Ciò significa che per testare B si inietta la connessione in B. Ma B viene istanziato in classe C e così via.

Quindi a che punto devo dire "qui prendo i dati da un database e non scriverò un test unitario per questo pezzo di codice"?

In altre parole: Da qualche parte nel codice in qualche classe I devo chiamare sqlDB.connect()o qualcosa di simile. Come testare questa classe?

Ed è lo stesso con il codice che ha a che fare con una GUI o un file system?


Voglio fare Unit-Test. Qualsiasi altro tipo di test non è correlato alla mia domanda. So che proverò solo una classe con esso (sono così d'accordo con te Kilian). Ora, alcune classi devono connettersi a un DB. Se voglio testare questa classe e chiedere "Come posso fare", molti dicono: "Usa l'iniezione di dipendenza!" Ma questo sposta il problema in un'altra classe, no? Quindi chiedo, come posso testare la classe che stabilisce davvero la connessione?

Domanda bonus: alcune risposte qui si riducono a "Usa oggetti finti!" Cosa significa? Derido le classi dalle quali dipende la classe sotto test. Devo deridere la classe sotto test ora e in realtà testare la simulazione (che si avvicina all'idea di utilizzare i metodi modello, vedi sotto)?


È la connessione al database che stai testando? La creazione di un database temporaneo nella memoria (come il derby ) sarebbe accettabile?

@MichaelT Devo ancora sostituire il database temporaneo in memoria con un database reale. Dove? Quando? Come viene testato l'unità? O è OK non testare questo codice?
TobiMcNamobi,

3
Non c'è nulla da "unit test" sulla base di dati. È gestito da altre persone e, se contenesse un bug, dovresti lasciarlo riparare piuttosto che farlo da solo. L'unica cosa che dovrebbe differire tra l'uso effettivo della classe e l'uso durante i test dovrebbe essere i parametri della connessione al database. È improbabile che il codice di lettura del file delle proprietà, o il meccanismo di iniezione di Spring o qualunque cosa tu usi per intrecciare la tua app sia rotto (e se fosse, non potresti ripararlo da solo, vedi sopra) - quindi lo considero accettabile non testare questo bit di funzionalità idraulica.
Kilian Foth,

2
@KilianFoth che è puramente correlato all'ambiente di lavoro e ai ruoli dei dipendenti. Non ha davvero nulla a che fare con la domanda. Cosa succede se non esiste una persona responsabile per il database?
Reactgular,

Alcuni framework di derisione consentono di iniettare oggetti simulati praticamente in qualsiasi cosa, anche in membri privati ​​e statici. Questo rende i test con cose come connessioni db finte molto più facili. Mockito + Powermock è ciò che funziona per me in questi giorni (sono Java, non sono sicuro di cosa tu stia lavorando).
FrustratedWithFormsDesigner,

Risposte:


21

Il punto di un unit test è testare una classe (in effetti, di solito dovrebbe testare un metodo ).

Ciò significa che quando si testa la classe A, si inserisce un database di prova in esso - qualcosa di scritto da sé o un database in memoria velocissimo, qualunque cosa faccia il lavoro.

Tuttavia, se testate la classe B, che è un client A, di solito prendete in giro l'intero Aoggetto con qualcos'altro, presumibilmente qualcosa che fa il suo lavoro in un modo primitivo e pre-programmato - senza usare un Aoggetto reale e certamente senza usare un dato base (a meno che non Ariporti al chiamante l'intera connessione alla base dati, ma è così orribile che non voglio pensarci). Allo stesso modo, quando scrivi un test unitario per la classe C, di cui è un cliente B, deridi qualcosa che assume il ruolo Be ti dimentichi del Atutto.

Se non lo fai, non è più un test unitario, ma un test di sistema o di integrazione. Anche quelli sono molto importanti, ma un bollitore di pesce completamente diverso. Per cominciare, di solito sono più impegnativi da installare ed eseguire, non è possibile richiedere il superamento come prerequisito per il check-in, ecc.


11

L'esecuzione di test unitari su una connessione al database è perfettamente normale e una pratica comune. Semplicemente non è possibile creare un puristapproccio in cui tutto nel sistema sia iniettabile per dipendenza.

La chiave qui è testare contro un database temporaneo o solo test e avere il processo di avvio più leggero possibile per costruire quel database di test.

Per i test unitari in CakePHP ci sono cose chiamate fixtures. Le partite sono tabelle temporanee di database create al volo per un unit test. L'apparecchio ha metodi convenienti per crearli. Possono ricreare uno schema da un database di produzione all'interno del database di test oppure è possibile definire lo schema usando una semplice notazione.

La chiave del successo con questo non è implementare il database aziendale, ma concentrarsi solo sull'aspetto del codice che si sta testando. Se si dispone di un unit test che verifica che un modello di dati legge solo i documenti pubblicati, lo schema della tabella per quel test dovrebbe avere solo i campi richiesti da quel codice. Non è necessario implementare nuovamente un intero database di gestione dei contenuti solo per testare quel codice.

Alcuni riferimenti aggiuntivi.

http://en.wikipedia.org/wiki/Test_fixture

http://phpunit.de/manual/3.7/en/database.html

http://book.cakephp.org/2.0/en/development/testing.html#fixtures


28
Non sono d'accordo. Un test che richiede una connessione al database non è un test unitario, poiché il test per sua stessa natura avrà effetti collaterali. Ciò non significa che non puoi scrivere un test automatizzato, ma un test di questo tipo è per definizione un test di integrazione, che esercita le aree del tuo sistema oltre la tua base di codice.
KeithS,

5
Chiamami purista, ma tengo fermamente che un unit test non dovrebbe eseguire alcuna azione che lasci la "sandbox" dell'ambiente di runtime di testing. Non dovrebbero toccare database, file system, socket di rete, ecc. Ciò è dovuto a diversi motivi, non ultimo quello della dipendenza del test dallo stato esterno. Un altro è la prestazione; la suite di test delle unità dovrebbe essere eseguita rapidamente e l'interfaccia con questi dati esterni memorizza i test lenti per ordini di grandezza. Nel mio sviluppo, uso simulazioni parziali per testare cose come i miei repository e mi sento a mio agio nel definire un "vantaggio" per il mio sandbox.
KeithS

2
@gbjbaanb - All'inizio suona bene, ma nella mia esperienza è molto pericoloso. Anche nelle suite di test e nei framework meglio progettati, il codice per il rollback di questa transazione potrebbe non essere eseguito. Se il test runner si arresta in modo anomalo o viene interrotto all'interno di un test o il test genera SOE o OOME, nel migliore dei casi è presente una connessione sospesa e una transazione nel DB che bloccherà i tavoli toccati fino a quando la connessione non viene interrotta. I modi in cui impedisci che ciò causi problemi, come l'utilizzo di SQLite come DB di prova, hanno i loro lati negativi, ad esempio il fatto che non stai realmente esercitando il vero DB.
KeithS,

5
@KeithS Penso che stiamo discutendo sulla semantica. Non si tratta di quale sia la definizione di un unit test o di un test di integrazione. Uso dispositivi per testare il codice in base a una connessione al database. Se questo è un test di integrazione, allora sto bene. Devo sapere che il test ha superato. Non mi importava di dipendenze, prestazioni o rischi. Non saprò se quel codice funziona a meno che il test non passi. Per la maggior parte dei test non ci sono dipendenze, ma per quelli in cui ci sono, tali dipendenze non possono essere disaccoppiate. È facile dire che dovrebbero essere, ma semplicemente non possono esserlo.
Reactgular,

4
Penso che lo siamo anche noi. Uso un "framework di unit test" (NUnit) anche per i miei test di integrazione, ma mi assicuro di separare queste due categorie di test (spesso in librerie separate). Il punto che stavo cercando di sottolineare è che la tua suite di test unitari, quella che esegui più volte al giorno prima di ogni check-in mentre segui la metodologia iterativa rossa-verde-refattore, dovrebbe essere completamente isolabile, in modo da poter eseguire questi test più volte al giorno senza calpestare le dita dei tuoi colleghi.
KeithS

4

C'è, da qualche parte nel tuo codebase, una riga di codice che esegue l'effettiva azione di connessione al DB remoto. Questa riga di codice è, 9 volte su 10, una chiamata a un metodo "incorporato" fornito dalle librerie di runtime specifiche per la tua lingua e il tuo ambiente. In quanto tale, non è il "tuo" codice e quindi non è necessario testarlo; ai fini di un unit test, ci si può fidare che questa chiamata al metodo funzionerà correttamente. Quello che puoi e dovresti ancora testare nella tua suite di test di unità sono cose come garantire che i parametri che verranno utilizzati per questa chiamata siano quelli che ti aspetti che siano, come assicurarti che la stringa di connessione sia corretta, o l'istruzione SQL o nome della procedura memorizzata.

Questo è uno degli scopi dietro la restrizione che i test unitari non dovrebbero lasciare il loro "sandbox" di runtime ed essere dipendenti dallo stato esterno. In realtà è abbastanza pratico; lo scopo di un unit test è verificare che il codice che hai scritto (o che stai per scrivere, in TDD) si comporti come pensavi. Il codice che non hai scritto, come la libreria che stai utilizzando per eseguire le operazioni del database, non dovrebbe far parte dell'ambito di nessun test unitario, per la semplice ragione che non l'hai scritto.

Nella tua suite di test di integrazione , queste restrizioni sono attenuate. Adesso puoitest di progettazione che tocchino il database, per assicurarsi che il codice che hai scritto funzioni bene con il codice che non hai. Queste due suite di test dovrebbero rimanere segregate, tuttavia, poiché la suite di test delle unità è più efficace quanto più velocemente viene eseguita (in modo da poter verificare rapidamente che tutte le affermazioni fatte dagli sviluppatori sul loro codice siano ancora valide) e quasi per definizione, un test di integrazione è più lento di ordini di grandezza a causa delle dipendenze aggiunte da risorse esterne. Lascia che il build-bot gestisca l'esecuzione della tua suite di integrazione completa ogni poche ore, eseguendo i test che bloccano le risorse esterne, in modo che gli sviluppatori non si calpestino reciprocamente eseguendo questi stessi test localmente. E se la build si rompe, e allora? Viene data molta più importanza a garantire che il build-bot non fallisca mai una build di quanto probabilmente dovrebbe essere.


Ora, quanto rigorosamente puoi aderire a questo dipende dalla tua strategia esatta per la connessione e l'interrogazione del database. In molti casi in cui è necessario utilizzare il framework di accesso ai dati "bare-bones", come gli oggetti SqlConnection e SqlStatement di ADO.NET, un intero metodo sviluppato dall'utente può consistere in chiamate di metodo integrate e altro codice che dipende dall'avere un connessione al database, quindi il meglio che puoi fare in questa situazione è deridere l'intera funzione e fidarti delle tue suite di test di integrazione. Dipende anche da quanto sei disposto a progettare le tue classi per consentire la sostituzione di specifiche righe di codice a scopo di test (come il suggerimento di Tobi sul modello Metodo del modello, che è buono perché consente "derisioni parziali"

Se il tuo modello di persistenza dei dati si basa sul codice nel tuo livello dati (come trigger, processi memorizzati, ecc.), Semplicemente non c'è altro modo per esercitare il codice che stai scrivendo se non quello di sviluppare test che vivono all'interno del livello dati o attraversano il confine tra il runtime dell'applicazione e il DBMS. Un purista direbbe che questo modello, per questo motivo, deve essere evitato a favore di qualcosa come un ORM. Non penso di andare così lontano; anche nell'era delle query integrate nella lingua e di altre operazioni di persistenza controllate dal compilatore e dipendenti dal dominio, vedo il valore di bloccare il database solo alle operazioni esposte tramite stored procedure e, naturalmente, tali procedure archiviate devono essere verificate utilizzando test. Ma tali test non sono test unitari . Sono integrazione test.

Se hai un problema con questa distinzione, di solito si basa su una grande importanza posta sulla "copertura del codice" completa, nota anche come "copertura del test unitario". Vuoi assicurarti che ogni riga del tuo codice sia coperta da un unit test. Un obiettivo nobile in faccia, ma dico lavanda; quella mentalità si presta ad anti-pattern che vanno ben oltre questo caso specifico, come scrivere test senza asserzioni che eseguono ma non si esercitanoil tuo codice. Questi tipi di end-run esclusivamente per motivi di numeri di copertura sono più dannosi che rilassare la copertura minima. Se vuoi assicurarti che ogni riga della tua base di codice venga eseguita da un test automatico, allora è facile; quando si calcolano le metriche di copertura del codice, includere i test di integrazione. Potresti anche fare un ulteriore passo avanti e isolare questi contestati test "Itino" ("Integrazione solo nel nome"), e tra la tua suite di test unitari e questa sottocategoria di test di integrazione (che dovrebbe comunque essere eseguita ragionevolmente velocemente) dovresti ottenere dannazione quasi vicino alla piena copertura.


2

I test unitari non devono mai connettersi a un database. Per definizione, dovrebbero testare una singola unità di codice ciascuno (un metodo) in totale isolamento dal resto del sistema. In caso contrario, non sono un test unitario.

Semantica a parte, ci sono una miriade di ragioni per cui questo è utile:

  • I test eseguono ordini di grandezza più velocemente
  • Il ciclo di feedback diventa istantaneo (<1s feedback per TDD, ad esempio)
  • I test possono essere eseguiti in parallelo per i sistemi di generazione / distribuzione
  • I test non richiedono l'esecuzione di un database (rende la creazione molto più semplice o almeno più rapida)

I test unitari sono un modo per controllare il tuo lavoro. Dovrebbero delineare tutti gli scenari per un determinato metodo, che in genere significa tutti i diversi percorsi attraverso un metodo. È la specifica che stai costruendo, simile alla contabilità a doppia entrata.

Quello che stai descrivendo è un altro tipo di test automatizzato: un test di integrazione. Anche se sono anche molto importanti, idealmente ne avrai molto meno. Dovrebbero verificare che un gruppo di unità si integri correttamente tra loro.

Quindi come testare le cose con l'accesso al database? Tutto il codice di accesso ai dati deve trovarsi in un livello specifico, in modo che il codice dell'applicazione possa interagire con servizi beffardo anziché con il database effettivo. Non dovrebbe importare se tali servizi sono supportati da qualsiasi tipo di database SQL, dati di test in memoria o persino dati di servizi Web remoti. Questa non è la loro preoccupazione.

Idealmente (e questo è molto soggettivo), si desidera che la maggior parte del codice sia coperta da test unitari. Questo ti dà la sicurezza che ogni pezzo funziona in modo indipendente. Una volta costruiti i pezzi, devi metterli insieme. Esempio: quando ho la password dell'utente, dovrei ottenere questo risultato esatto.

Diciamo che ogni componente è composto da circa 5 classi: vorresti testare tutti i punti di errore al loro interno. Ciò equivale a molti meno test solo per garantire che tutto sia cablato correttamente. Esempio: prova che puoi trovare l'utente dal database con un nome utente / password.

Infine, vuoi alcuni test di accettazione per assicurarti effettivamente di raggiungere gli obiettivi aziendali. Ce ne sono ancora meno; possono garantire che l'applicazione sia in esecuzione e fare ciò per cui è stata creata. Esempio: dati questi dati di prova, dovrei essere in grado di accedere.

Pensa a questi tre tipi di test come a una piramide. Hai bisogno di molti test unitari per supportare tutto, e poi sali da lì.


1

Il modello Metodo modello potrebbe essere d'aiuto.

Si avvolgono le chiamate a un database in protectedmetodi. Per testare questa classe, si verifica effettivamente un oggetto falso che eredita dalla classe di connessione del database reale e ignora i metodi protetti.

In questo modo le chiamate effettive al database non vengono mai sottoposte a unit test, giusto. Ma sono solo queste poche righe di codice. E questo è accettabile.


1
Nel caso ti chiedi perché rispondo alla mia domanda: Sì, questa potrebbe essere una risposta ma non sono abbastanza sicuro che sia quella giusta.
TobiMcNamobi,

-1

Il test con dati esterni è un test di integrazione. Unit test significa che stai testando solo l'unità. Viene fatto principalmente con la tua logica aziendale. Per rendere testabile la tua unità di codice devi seguire alcune linee guida, come devi rendere la tua unità indipendente dall'altra parte del tuo codice. Durante il test unitario, se sono necessari dati, è necessario iniettarli con forza con l'iniezione di dipendenza. Ci sono alcune strutture beffardo e beffardo là fuori.

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.