Domanda

Ho sentito dire che il test unitario è "assolutamente fantastico", "davvero interessante" e "ogni sorta di cose buone", ma il 70% o più dei miei file implica l'accesso al database (alcuni in lettura e altri in scrittura) 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/tutti i linguaggi che utilizzano l'accesso al database.

È stato utile?

Soluzione

Suggerirei di prendere in giro le tue chiamate al database.I mock sono fondamentalmente oggetti che assomigliano all'oggetto su cui stai tentando di chiamare un metodo, nel senso che hanno le stesse proprietà, metodi, ecc.disponibile per il chiamante.Ma invece di eseguire qualsiasi azione per cui sono programmati quando viene chiamato un particolare metodo, lo salta del tutto e restituisce semplicemente un risultato.Tale risultato viene generalmente definito da te in anticipo.

Per impostare i tuoi oggetti per il mocking, probabilmente dovrai utilizzare una sorta di inversione del modello di iniezione di 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 test unitario crei un mock di FooDataProvider, che ti consente di chiamare il metodo GetAllFoos senza dover effettivamente raggiungere 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 sottoporre a test unitario anche le tue effettive chiamate al database, per le quali dovrai accedere al database.

Altri suggerimenti

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

Se i tuoi oggetti sono strettamente associati al livello dati, è difficile eseguire test unitari 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 risiedono nel modello di dominio principale e sono accessibili dal mio livello di applicazione.Il livello dell'applicazione comunica sia con il livello dei dati che con il livello del modello di dominio.

Il livello dell'applicazione è talvolta chiamato anche "livello aziendale".

Se utilizzi PHP, crea un set specifico di classi per SOLTANTO accesso ai dati.Assicurati che i tuoi oggetti non abbiano idea di come vengono persistenti e collega i due nelle classi dell'applicazione.

Un'altra opzione sarebbe quella di utilizzare mocking/stub.

Il modo più semplice per eseguire test unitari di un oggetto con accesso al database è utilizzare gli ambiti delle transazioni.

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 grandi progetti.La nostra build richiede un po' di tempo per essere eseguita (15 minuti), ma non è terribile per avere 1800 unit test.Inoltre, se il tempo di compilazione è un problema, puoi modificare il processo di compilazione per avere più build, una per la creazione di src, un'altra che si avvia successivamente per gestire test unitari, analisi del codice, packaging, ecc...

Dovresti deridere l'accesso al database se vuoi testare le tue classi.Dopotutto, non vuoi testare il database in uno unit test.Sarebbe un test di integrazione.

Astrarre le chiamate e quindi inserire un mock che restituisca semplicemente i dati attesi.Se le tue classi non fanno altro che eseguire query, potrebbe non valere nemmeno la pena testarle...

Forse posso darti un assaggio della nostra esperienza quando abbiamo iniziato a esaminare le unità di test del nostro processo di livello intermedio che includeva un sacco di operazioni SQL di "logica aziendale".

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

Una volta installato questo, siamo stati in grado di fare qualcosa di simile nel nostro codice (lavoriamo in C++, ma sono sicuro che tu abbia capito):

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

In fase di esecuzione normale, GetDatabase() restituiva un oggetto che alimentava tutto il nostro SQL (comprese le query), tramite ODBC direttamente al database.

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

Siamo ancora nelle fasi iniziali, ma finora sembra buono, tuttavia dobbiamo assicurarci di creare tutte le tabelle richieste e popolarle con i dati di test, tuttavia qui abbiamo ridotto in qualche modo il carico di lavoro creando un insieme generico di funzioni di supporto che possono fare molto di tutto questo per noi.

Nel complesso, ci ha aiutato immensamente con il nostro processo TDD, poiché apportare modifiche che sembrano abbastanza 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 incentrate su un ambiente di sviluppo C++, tuttavia sono sicuro che potresti ottenere qualcosa di simile lavorando con PHP/Python.

Spero che questo ti aiuti.

Il libro Modelli di prova xUnit descrive alcuni modi per gestire il codice di unit test che colpisce un database.Sono d'accordo con le altre persone che dicono che non vuoi farlo perché è lento, ma devi farlo prima o poi, IMO.Prendere in giro 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.

Opzioni che hai:

  • Scrivi uno script che cancellerà il database prima di avviare i test unitari, quindi popolare il db con un set di dati predefinito ed esegui 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 tutti i linguaggi OO)

    class Database {
     public Result query(String query) {... real db here ...}
    }

    class mockdatabase estende il database {query di risultati pubblici (query string) {return "risultato mock";}}

    class ObjectThaTusSdb {ObjectThaTusSdb pubblici (database db) {this.database = db;}}

    ora in produzione usi il database normale e per tutti i test ti basta inserire il database mock che puoi creare ad hoc.

  • Non utilizzare affatto DB nella maggior parte del codice (è comunque una cattiva pratica).Crea un oggetto "database" che invece di restituire risultati restituirà oggetti normali (ad es.sarà di ritorno User invece di una tupla {name: "marcin", password: "blah"}) scrivi tutti i tuoi test con costruiti ad hoc vero oggetti e scrivere un grande test che dipende da un database che assicura che questa conversione funzioni correttamente.

Naturalmente questi approcci non si escludono a vicenda e puoi mescolarli e abbinarli secondo le tue necessità.

Di solito cerco di suddividere i miei test tra il test degli oggetti (e l'ORM, se presente) e il test del db.Metto alla prova 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, secondo la mia esperienza, di solito è abbastanza limitato.

Ero frustrato dalla scrittura di unit test fino a quando non ho iniziato a deridere la parte di accesso ai dati, quindi non dovevo creare un database di test o generare dati di test al volo.Deridendo i dati puoi generarli tutti in fase di esecuzione ed essere sicuro che i tuoi oggetti funzionino correttamente con input noti.

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 implementarne alcuni CIO sia che si tratti di uno strumento di terze parti o che lo gestisci tu stesso, puoi implementare una versione fittizia del chiamante del database da cui controllerai l'esito di quella chiamata falsa.

Una forma semplice di IoC può essere eseguita semplicemente codificando in Interfaces.Ciò richiede una sorta di orientamento agli oggetti nel tuo codice, quindi potrebbe non applicarsi a ciò che stai facendo (lo dico poiché tutto ciò che devo fare è la tua menzione di PHP e Python)

Spero che sia utile, se non altro hai alcuni termini su cui cercare ora.

Sono d'accordo 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 rispetto a un'implementazione stub del livello DAO.

Potresti usare quadri beffardi per astrarre il motore del database.Non so se PHP/Python ne abbia alcuni, ma per i linguaggi tipizzati (C#, Java ecc.) ci sono molte scelte

Dipende anche da come hai progettato il codice di accesso al database, perché alcuni progetti sono più facili da testare rispetto ad altri come menzionato nei post precedenti.

Testare l'unità per l'accesso al database è abbastanza semplice se il tuo progetto ha un'elevata coesione e un accoppiamento lento in tutto.In questo modo puoi testare solo le cose che fa ciascuna classe particolare senza dover testare tutto in una volta.

Ad esempio, se esegui un test unitario della classe dell'interfaccia utente, i test che scrivi dovrebbero 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 tale funzione.

Se desideri testare l'unità dell'accesso effettivo al database, ti ritroverai con più di un test di integrazione, perché dipenderai dallo stack di rete e dal server del database, ma puoi verificare che il tuo codice SQL faccia ciò che gli hai chiesto 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 farei senza di essi.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 inviare che descrive una tecnica che ho utilizzato per eseguire gli stessi test.

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

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top