Test-unità e database: A che punto mi collego effettivamente al database?

32

Ci sono risposte alla domanda su come le classi di test che si connettono a un database, ad es. "Le classi di test di servizio devono connettersi ..." e "Test dell'unità - Applicazione accoppiata database" .

Quindi, in breve, supponiamo di avere una classe A che deve connettersi a un database. Invece di consentire a A di connettersi effettivamente, fornisci A con un'interfaccia che A può usare per connettersi. Per testare l'implementazione di questa interfaccia con alcune cose - senza connessione, naturalmente. Se la classe B crea un'istanza di A, deve passare una connessione di database "reale" ad A. Ma ciò significa che B apre una connessione al database. Ciò significa testare B e iniettare la connessione in B. Ma B è 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 deve chiamare sqlDB.connect() o qualcosa di simile. Come posso testare questo corso?

E lo stesso vale per il codice che ha a che fare con una GUI o un file system?

Modifica: Finora ci sono delle belle risposte, ma ho la sensazione di non essermi reso molto chiaro. Un ultimo approccio:

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

Domanda bonus: alcune risposte qui si riducono a "Usa oggetti finti!" Cosa significa? Faccio schifo a classi che dipendono dal sottotest della classe. Devo prendere in giro il sottotest della classe ora e testare effettivamente il mock (che si avvicina all'idea di usare i metodi di template, vedi sotto)?

    
posta TobiMcNamobi 30.07.2013 - 16:15
fonte

6 risposte

1

Il modello di metodo dei modelli potrebbe essere d'aiuto.

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

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

    
risposta data 30.07.2013 - 16:30
fonte
19

Il punto di un test unitario è di testare una classe (infatti, di solito dovrebbe testare un metodo ).

Questo significa che quando si prova la classe A , si inserisce un database di test in esso - qualcosa di auto-scritto, o un database in memoria fulmineo, qualunque cosa faccia il lavoro.

Tuttavia, se provi la classe B , che è un client di A , di solito prendi in giro l'intero oggetto A con qualcos'altro, presumibilmente qualcosa che fa il suo lavoro in modo primitivo e pre-programmato - senza utilizzare un oggetto A effettivo e sicuramente senza utilizzare un database (a meno che A non restituisca l'intera base di dati al suo chiamante, ma è così orribile che non voglio pensarci). Allo stesso modo, quando scrivi un test unitario per la classe C , che è un client di B , prendi in giro qualcosa che assume il ruolo di B e dimentica del tutto A del tutto.

Se non lo fai, non è più un test unitario, ma un test di sistema o di integrazione. Anche quelli sono molto importanti, ma un intero assortimento di pesci. Per cominciare, di solito sono più sforzi da impostare e da eseguire, non è praticabile pretenderne il passaggio come precondizione per il check-in, ecc.

    
risposta data 30.07.2013 - 16:24
fonte
9

L'esecuzione di test unitari contro una connessione al database è perfettamente normale e una pratica comune. Semplicemente non è possibile creare un approccio purist in cui tutto nel tuo sistema è iniezioni di dipendenza.

La chiave qui è di testare un database temporaneo o di prova 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 fixture sono tabelle di database temporanee create al volo per un test unitario. L'apparecchio ha metodi convenienti per crearli. Possono ricreare uno schema da un database di produzione all'interno del database di test, oppure puoi 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 test unitario che verifica che un modello di dati legga solo i documenti pubblicati, lo schema di tabella per quel test dovrebbe avere solo i campi richiesti da quel codice. Non è necessario ri-implementare un intero database di gestione dei contenuti solo per testare quel codice.

Alcuni riferimenti aggiuntivi.

link

link

link

    
risposta data 30.07.2013 - 16:52
fonte
3

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

Questo è uno degli scopi alla base della limitazione secondo cui i test unitari non devono lasciare la loro "sandbox" di runtime e dipendere dallo stato esterno. In realtà è abbastanza pratico; lo scopo di un test unitario è verificare che il codice che hai scritto (o che stai per scrivere, in TDD) si comporti nel modo in cui pensavi che sarebbe successo. Il codice che non hai scritto, come ad esempio la libreria che stai utilizzando per eseguire le operazioni del tuo database, non dovrebbe far parte dello scope di alcun test unitario, per il semplice motivo che non lo hai scritto.

Nella tua suite di test integration , queste restrizioni sono rilassate. Ora tu puoi test di progettazione che toccano il database, per assicurarti 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 e più veloce è la sua esecuzione (in modo da poter verificare rapidamente che tutte le asserzioni 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 alle risorse esterne. Lascia che il build-bot gestisca l'esecuzione della suite di integrazione completa ogni poche ore, eseguendo i test che bloccano le risorse esterne, in modo che gli sviluppatori non si calpestino l'un l'altro eseguendo gli stessi test localmente. E se la costruzione si rompe, e allora? È molto più importante 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 esatta strategia 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 metodi incorporate e altro codice che dipende dall'avere un connessione al database, e quindi il meglio che si possa fare in questa situazione è prendere in giro l'intera funzione e fidarsi delle suite di test di integrazione. Dipende anche da quanto sei disposto a progettare le tue classi per consentire che specifiche linee di codice vengano sostituite a scopo di test (come il suggerimento di Tobi del modello Method Method, che è buono perché permette "parolacce parziali" che esercitano metodi di una vera classe ignorando gli altri che hanno effetti collaterali).

Se il tuo modello di persistenza dei dati si basa sul codice nel tuo livello dati (come trigger, stored proc, ecc) allora semplicemente non c'è altro modo di esercitare il codice che stai scrivendo piuttosto che sviluppare test che vivono all'interno del livello dati o attraversare 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 che andrei così lontano; anche nell'era delle query integrate nella lingua e di altre operazioni di persistenza controllate dal dominio e dipendenti dal dominio, vedo il valore nel bloccare il database solo per le operazioni esposte tramite stored procedure e, naturalmente, tali stored procedure devono essere verificate mediante l'automazione test. Ma tali test non sono test unit . Sono test integration .

Se hai un problema con questa distinzione, di solito si basa su una grande importanza posta sulla completa "copertura del codice" alias "copertura del test unitario". Volete assicurarvi che ogni riga del vostro codice sia coperta da un test unitario. Un nobile obiettivo sul suo volto, ma io dico sciocchezza; questa mentalità si presta a anti-modelli che vanno ben al di là di questo caso specifico, come la scrittura di test privi di asserzione che eseguono ma non esercitano il 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 del tuo codebase sia eseguita da qualche 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 test "Itino" contestati ("Integrazione solo in nome"), e tra la tua suite di test unitaria e questa sottocategoria di test di integrazione (che dovrebbe comunque funzionare abbastanza velocemente) dovresti ottenere maledettamente vicino a una copertura completa.

    
risposta data 30.07.2013 - 18:22
fonte
1

I test unitari non dovrebbero mai connettersi a un database. Per definizione, dovrebbero testare una singola unità di codice ciascuna (un metodo) in totale isolamento dal resto del sistema. Se non lo fanno, allora non sono un test unitario.

A parte la semantica, ci sono una miriade di motivi per cui questo è utile:

  • I test eseguono ordini di grandezza più veloci
  • Il ciclo di feedback diventa istantaneo (< 1s feedback per TDD, ad esempio)
  • I test possono essere eseguiti in parallelo per costruire / implementare sistemi
  • I test non hanno bisogno di un database per essere eseguito (rende la compilazione molto più semplice, o almeno più veloce)

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

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 essere in un livello specifico, in modo che il codice dell'applicazione possa interagire con servizi discutibili 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), vuoi che la maggior parte del tuo codice sia coperta dai test unitari. Questo ti dà la sicurezza che ogni pezzo lavori in modo indipendente. Una volta che i pezzi sono stati costruiti, devi metterli insieme. Esempio: quando ho cancellato 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 puoi trovare l'utente dal database con nome utente / password.

Infine, si desidera che alcuni test di accettazione garantiscano effettivamente il raggiungimento degli obiettivi aziendali. Ce ne sono anche di meno; possono assicurarsi che l'applicazione sia in esecuzione e faccia ciò che è stato creato per fare. Esempio: dati questi dati di prova, dovrei riuscire ad accedere.

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

    
risposta data 30.07.2013 - 18:51
fonte
-1

Il test con dati esterni è un test di integrazione. Test unitario significa che stai testando solo l'unità. È per lo più fatto con la tua logica di business. Per rendere testabile la tua unità di codice devi seguire alcune linee guida, come devi rendere la tua unità indipendente da un'altra parte del tuo codice. Durante il test unitario se hai bisogno di dati, devi iniettarli forzatamente con un'iniezione di dipendenza. Ci sono alcune strutture di derisione e di stoppamento là fuori.

    
risposta data 01.08.2013 - 17:49
fonte

Leggi altre domande sui tag