Come testare un'unità di un oggetto con le query del database


153

Ho sentito che i test delle unità sono "assolutamente fantastici", "davvero fantastici" e "ogni sorta di cose buone" ma il 70% o più dei miei file comporta l'accesso al database (alcuni letti e altri scritti) e non sono sicuro di come per scrivere un test unitario per questi file.

Sto usando PHP e Python ma penso che sia una domanda che si applica alla maggior parte / tutte le lingue che usano l'accesso al database.

Risposte:


82

Suggerirei di deridere le tue chiamate al database. I mock sono fondamentalmente oggetti che assomigliano all'oggetto su cui stai cercando di chiamare un metodo, nel senso che hanno le stesse proprietà, metodi, ecc. Disponibili per il chiamante. Ma invece di eseguire qualsiasi azione siano programmati per fare quando viene chiamato un metodo particolare, lo salta del tutto e restituisce semplicemente un risultato. Tale risultato è in genere definito dall'utente in anticipo.

Per impostare i tuoi oggetti per deridere, probabilmente dovrai usare una sorta di inversione del modello di iniezione controllo / dipendenza, come nel seguente pseudo-codice:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Ora nel tuo unit test, crei una simulazione di FooDataProvider, che ti consente di chiamare il metodo GetAllFoos senza dover effettivamente colpire il database.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Uno scenario beffardo comune, in poche parole. Ovviamente probabilmente vorrai comunque provare anche tu le unità delle tue effettive chiamate al database, per le quali dovrai colpire il database.


So che questo è vecchio, ma per quanto riguarda la creazione di una tabella duplicata a quella già presente nel DB. In questo modo puoi confermare che le chiamate DB funzionano?
Bretterer

1
Sto usando il PDO di PHP come accesso di livello più basso al database, su cui ho estratto un'interfaccia. Quindi ho creato un livello di database compatibile con l'applicazione. Questo è il livello che contiene tutte le query SQL non elaborate e altre informazioni. Il resto dell'applicazione interagisce con questo database di livello superiore. Ho trovato che funziona abbastanza bene per i test unitari; Metto alla prova le mie pagine di applicazione su come interagiscono con il database dell'applicazione. Metto alla prova il mio database di applicazioni su come interagisce con il DOP. Presumo che il DOP funzioni senza errori. Codice sorgente: manx.codeplex.com
legalizza il

1
@bretterer: la creazione di una tabella duplicata è utile per i test di integrazione. Per i test unitari di solito si utilizza un oggetto simulato che consente di testare un'unità di codice indipendentemente dal database.
BornToCode

2
Qual è il valore nel deridere le chiamate al database nei test delle unità? Non sembra utile perché è possibile modificare l'implementazione per restituire un risultato diverso, ma il test dell'unità passerebbe (erroneamente).
bmay2

2
@ bmay2 Non sbagli. La mia risposta originale è stata scritta molto tempo fa (9 anni!) Quando molte persone non scrivevano il loro codice in modo testabile e quando gli strumenti di test erano gravemente carenti. Non consiglierei più questo approccio. Oggi vorrei semplicemente creare un database di test e popolarlo con i dati di cui avevo bisogno per il test, e / o progettare il mio codice in modo da poter testare quanta più logica possibile senza un database.
Doug R,

25

Idealmente, i tuoi oggetti dovrebbero essere persistenti ignoranti. Ad esempio, dovresti avere un "livello di accesso ai dati", a cui faresti richieste, che restituisca oggetti. In questo modo, puoi lasciare quella parte fuori dai test unitari o provarli separatamente.

Se gli oggetti sono strettamente accoppiati al livello dati, è difficile eseguire test di unità adeguati. la prima parte del test unitario è "unit". Tutte le unità dovrebbero poter essere testate isolatamente.

Nei miei progetti c #, utilizzo NHibernate con un livello dati completamente separato. I miei oggetti vivono nel modello di dominio principale e sono accessibili dal mio livello applicazione. Il livello applicazione parla sia al livello dati che al livello del modello di dominio.

Il livello dell'applicazione viene talvolta chiamato anche "Livello aziendale".

Se si utilizza PHP, creare un set specifico di classi per l'accesso SOLO ai dati. Assicurati che i tuoi oggetti non abbiano idea di come siano persistenti e collega i due nelle tue classi di applicazione.

Un'altra opzione sarebbe quella di usare beffe / stub.


Sono sempre stato d'accordo con questo, ma in pratica a causa delle scadenze e "ok, ora aggiungo solo un'altra funzionalità, entro le 14:00 di oggi" questa è una delle cose più difficili da raggiungere. Questo genere di cose è un obiettivo primario del refactoring, tuttavia, se il mio capo dovesse mai decidere di non aver pensato a 50 nuovi problemi emergenti che richiedono nuove logiche e tabelle di business.
Darren Ringer,

3
Se gli oggetti sono strettamente accoppiati al livello dati, è difficile eseguire test di unità adeguati. la prima parte del test unitario è "unit". Tutte le unità dovrebbero poter essere testate isolatamente. bella spiegazione
Amitābha,

11

Il modo più semplice per testare un'unità di un oggetto con accesso al database è utilizzare gli ambiti di transazione.

Per esempio:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Ciò ripristinerà lo stato del database, sostanzialmente come un rollback delle transazioni in modo da poter eseguire il test tutte le volte che vuoi senza effetti collaterali. Abbiamo utilizzato questo approccio con successo in progetti di grandi dimensioni. La costruzione richiede un po 'di tempo (15 minuti), ma non è orribile per avere 1800 test unitari. Inoltre, se il tempo di compilazione è un problema, è possibile modificare il processo di compilazione in modo da disporre di più build, una per la creazione di SRC, un'altra che si attiva in seguito e gestisce test unitari, analisi del codice, packaging, ecc ...


1
+1 Risparmia molto tempo durante il test di unità dei livelli di accesso ai dati. Basta notare che TS avrà spesso bisogno di MSDTC che potrebbe non essere desiderabile (a seconda se l'app avrà bisogno di MSDTC)
StuartLC

La domanda originale era su PHP, questo esempio sembra essere C #. Gli ambienti sono molto diversi.
legalizzare il

2
L'autore della domanda ha affermato che si tratta di una domanda generale che si applica a tutte le lingue che hanno a che fare con un DB.
Vedran,

9
e questo cari amici, si chiama test di integrazione
AA.

10

Posso forse darti un assaggio della nostra esperienza quando abbiamo iniziato a esaminare i test unitari del nostro processo di livello intermedio che includeva una tonnellata di operazioni sql "business logic".

Per prima cosa abbiamo creato un livello di astrazione che ci ha permesso di "inserire" qualsiasi connessione di database ragionevole (nel nostro caso, abbiamo semplicemente supportato una singola connessione di tipo ODBC).

Una volta che questo è stato messo in atto, siamo stati in grado di fare qualcosa del genere nel nostro codice (lavoriamo in C ++, ma sono sicuro che avrai l'idea):

GetDatabase (). ExecuteSQL ("INSERT INTO foo (blah, blah)")

In fase di esecuzione normale, GetDatabase () restituiva un oggetto che alimentava tutti i nostri sql (comprese le query), tramite ODBC direttamente al database.

Abbiamo quindi iniziato a esaminare i database in memoria: il migliore di gran lunga sembra essere SQLite. ( http://www.sqlite.org/index.html ). È straordinariamente semplice da configurare e utilizzare e ci ha consentito di eseguire la sottoclasse e sovrascrivere GetDatabase () per inoltrare sql a un database in memoria creato e distrutto per ogni test eseguito.

Siamo ancora nelle prime fasi di questo, ma finora sembra buono, tuttavia dobbiamo assicurarci di creare tutte le tabelle necessarie e popolarle con i dati di test - tuttavia abbiamo ridotto il carico di lavoro qui creando un insieme generico di funzioni di supporto che può fare molto di tutto questo per noi.

Nel complesso, ha aiutato immensamente il nostro processo TDD, dal momento che apportare modifiche apparentemente innocue per correggere alcuni bug può avere effetti piuttosto strani su altre aree (difficili da rilevare) del tuo sistema - a causa della natura stessa di sql / database.

Ovviamente, le nostre esperienze sono state incentrate su un ambiente di sviluppo C ++, tuttavia sono sicuro che potresti riuscire a far funzionare qualcosa di simile in PHP / Python.

Spero che questo ti aiuti.


9

È necessario deridere l'accesso al database se si desidera testare l'unità delle classi. Dopotutto, non si desidera testare il database in un unit test. Sarebbe un test di integrazione.

Estrarre le chiamate e quindi inserire un finto che restituisce solo i dati previsti. Se le tue classi non fanno altro che eseguire query, potrebbe non valere la pena testarle, anche se ...


6

Il libro xUnit Test Patterns descrive alcuni modi per gestire il codice di unit test che colpisce un database. Sono d'accordo con le altre persone che stanno dicendo che non vuoi farlo perché è lento, ma devi farlo prima o poi, IMO. Deridere la connessione db per testare cose di livello superiore è una buona idea, ma dai un'occhiata a questo libro per suggerimenti su cose che puoi fare per interagire con il database reale.


4

Opzioni che hai:

  • Scrivere uno script che cancellerà il database prima di iniziare i test unitari, quindi popolare db con un set di dati predefinito ed eseguire i test. Puoi anche farlo prima di ogni test: sarà lento, ma meno soggetto a errori.
  • Iniettare il database. (Esempio in pseudo-Java, ma si applica a tutte le lingue OO)

    database di classe {
     query di risultati pubblica (query di stringa) {... real db here ...}
    }

    classe MockDatabase estende Database { query di risultati pubblica (query di stringa) { restituisce "finto risultato"; } }

    class ObjectThatUsesDB { public ObjectThatUsesDB (Database db) { this.database = db; } }

    ora in produzione si utilizza un normale database e per tutti i test basta iniettare il finto database che è possibile creare ad hoc.

  • Non usare DB per la maggior parte del codice (è comunque una cattiva pratica). Crea un oggetto "database" che invece di restituire con risultati restituirà oggetti normali (cioè restituirà Userinvece di una tupla {name: "marcin", password: "blah"}) scrivi tutti i tuoi test con oggetti reali costruiti ad hoc e scrivi un test grande che dipende da un database che si assicura che questa conversione funziona bene.

Naturalmente questi approcci non si escludono a vicenda e puoi mescolarli e abbinarli di cui hai bisogno.


3

L'unità di test dell'accesso al database è abbastanza semplice se il progetto ha una coesione elevata e un accoppiamento allentato in tutto. In questo modo puoi testare solo le cose che ogni classe specifica fa senza dover testare tutto in una volta.

Ad esempio, se si esegue il test unitario della classe dell'interfaccia utente, i test scritti devono solo provare a verificare che la logica all'interno dell'interfaccia utente funzioni come previsto, non la logica aziendale o l'azione del database dietro quella funzione.

Se si desidera testare l'unità dell'effettivo accesso al database, si finirà effettivamente con più di un test di integrazione, poiché si dipenderà dallo stack di rete e dal server del database, ma è possibile verificare che il codice SQL esegua ciò che è stato richiesto fare.

Il potere nascosto dei test unitari per me personalmente è stato che mi costringe a progettare le mie applicazioni in un modo molto migliore di quanto potrei fare senza di loro. Questo perché mi ha davvero aiutato a staccarmi dalla mentalità "questa funzione dovrebbe fare tutto".

Mi dispiace non ho esempi di codice specifici per PHP / Python, ma se vuoi vedere un esempio .NET ho un post che descrive una tecnica che ho usato per fare lo stesso test.


2

Di solito cerco di interrompere i miei test tra testare gli oggetti (e ORM, se presente) e testare il db. Collaudo il lato oggetto delle cose deridendo le chiamate di accesso ai dati mentre collaudo il lato db delle cose testando le interazioni dell'oggetto con il db che, nella mia esperienza, di solito è abbastanza limitato.

Ero frustrato con la scrittura di unit test fino a quando non iniziavo a deridere la parte di accesso ai dati, quindi non dovevo creare un db di prova o generare dati di test al volo. Deridendo i dati è possibile generarli tutti in fase di esecuzione ed assicurarsi che gli oggetti funzionino correttamente con input noti.


2

Non l'ho mai fatto in PHP e non ho mai usato Python, ma quello che vuoi fare è deridere le chiamate al database. Per fare ciò puoi implementare un IoC sia che si tratti di uno strumento di terze parti o lo gestisci tu stesso, quindi puoi implementare una versione simulata del chiamante del database che è dove controllerai il risultato di quella chiamata falsa.

Una semplice forma di IoC può essere eseguita semplicemente codificando le interfacce. Ciò richiede un certo tipo di orientamento degli oggetti nel tuo codice, quindi potrebbe non applicarsi a quello che stai facendo (dico che dato che tutto ciò che devo fare è menzionare PHP e Python)

Spero sia utile, se non altro hai qualche termine su cui cercare adesso.


2

Concordo con il primo post: l'accesso al database dovrebbe essere rimosso in un livello DAO che implementa un'interfaccia. Quindi, puoi testare la tua logica contro un'implementazione di stub del livello DAO.


2

È possibile utilizzare framework di simulazione per sottrarre il motore di database. Non so se PHP / Python ne abbia alcuni ma per i linguaggi digitati (C #, Java ecc.) Ci sono molte scelte

Dipende anche da come hai progettato quei codici di accesso al database, perché alcuni progetti sono più facili da testare rispetto ad altri come quelli menzionati in precedenza.


2

L'impostazione dei dati di test per unit test può essere una sfida.

Quando si tratta di Java, se si utilizzano API Spring per i test delle unità, è possibile controllare le transazioni a livello di unità. In altre parole, è possibile eseguire unit test che comportano aggiornamenti / inserimenti / eliminazioni del database e rollback delle modifiche. Alla fine dell'esecuzione lasci tutto nel database com'era prima di iniziare l'esecuzione. Per me, è il massimo che può ottenere.

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.