I test unitari non dovrebbero usare i miei metodi?

80

Oggi stavo guardando un video " JUnit basi" e l'autore ha detto che durante il test di un determinato metodo nel tuo programma , non dovresti usare altri metodi propri nel processo.

Per essere più specifico, stava parlando di testare alcuni metodi di creazione dei record che hanno preso un nome e un cognome per gli argomenti, e li ha usati per creare record in una data tabella. Ma ha affermato che nel processo di testare questo metodo, non dovrebbe usare i suoi altri metodi DAO per interrogare il database su controlla il risultato finale (per verificare che il record sia stato effettivamente creato con i dati corretti). Ha affermato che per questo, dovrebbe scrivere un ulteriore codice JDBC per interrogare il database e controllare il risultato.

Penso di capire lo spirito della sua affermazione: non si vuole che il test case di un metodo dipenda dalla correttezza degli altri metodi (in questo caso, il metodo DAO), e ciò si ottiene scrivendo (di nuovo) il tuo codice di validazione / supporto (che dovrebbe essere più specifico e focalizzato, quindi, un codice più semplice).

Tuttavia, le voci nella mia testa iniziarono a protestare con argomenti come la duplicazione del codice, inutili sforzi aggiuntivi, ecc. Voglio dire, se eseguiamo l'intera batteria di test, testiamo tutti i nostri metodi pubblici (incluso il metodo DAO in questo caso ), non dovrebbe essere giusto usare solo alcuni di questi metodi mentre si testano altri metodi? Se uno di loro non sta facendo quello che dovrebbe, allora il suo caso di test fallirà, e possiamo ripararlo ed eseguire di nuovo la batteria di test. Nessuna necessità di duplicazione del codice (anche se il codice duplicato è in qualche modo più semplice) o spreco di sforzi.

Ho un strong sentimento a causa di diversi recenti Excel - VBA applicazioni che ho scritto (correttamente testato per unità grazie a Rubberduck per VBA ), dove applicare questa raccomandazione significherebbe un sacco di lavoro supplementare aggiuntivo, senza alcun beneficio percepito.

Puoi condividere le tue opinioni su questo?

    
posta carlossierra 06.09.2016 - 17:40
fonte

11 risposte

186

Lo spirito della sua richiesta è davvero corretto. Il punto di test unitario è di isolare il codice, testarlo privo di dipendenze, in modo che qualsiasi comportamento errato possa essere rapidamente riconosciuto dove sta accadendo.

Detto ciò, il test unitario è uno strumento, ed è pensato per servire ai tuoi scopi, non è un altare da pregare. A volte questo significa abbandonare le dipendenze perché funzionano in modo abbastanza affidabile e non vuoi disturbarti a deriderle, a volte questo significa che alcuni dei tuoi test unitari sono in realtà piuttosto vicini se non addirittura test di integrazione.

Alla fine non ti viene assegnato un punteggio, ciò che è importante è il prodotto finale del software che stai testando, ma devi solo prestare attenzione quando stai piegando le regole e decidi quando i trade-off sono ne vale la pena.

    
risposta data 06.09.2016 - 18:55
fonte
36

Penso che questo dipenda dalla terminologia. Per molti, un "unit test" è una cosa molto specifica, e per definizione non può avere una condizione pass / fail che dipende da qualsiasi codice al di fuori dell'unità (metodo , funzione, ecc.) in fase di test. Ciò includerebbe l'interazione con un database.

Per gli altri, il termine "test unitario" è molto più flessibile e comprende qualsiasi tipo di test automatizzato, incluso il codice di test che verifica le porzioni integrate dell'applicazione.

Un purista di test (se posso usare quel termine) potrebbe chiamare un test di integrazione , distinto da un test di unità sulla base del fatto che il test dipende da più di una pura unità di codice.

Sospetto che tu stia utilizzando la versione più flessibile del termine "test unitario" e in realtà si riferisce a un "test di integrazione".

Usando queste definizioni, non dovresti dipendere dal codice al di fuori dell'unità sottoposta a test in un "test unitario". In un "test di integrazione", tuttavia, è perfettamente ragionevole scrivere un test che eserciti una parte specifica del codice e quindi ispeziona il database per i criteri di passaggio.

Se si debba dipendere da test di integrazione o test unitari o entrambi sono argomenti di discussione molto più ampi.

    
risposta data 06.09.2016 - 19:13
fonte
7

La risposta è sì e no ...

I test univoci che si svolgono in isolamento sono assolutamente necessari in quanto consentono di definire il lavoro di base che il codice di basso livello funziona in modo appropriato. Ma nella codifica di librerie più grandi, troverai anche aree di codice che richiedono che tu abbia dei test che attraversino le unità.

Questi test inter-unità sono utili per la copertura del codice e quando si testano le funzionalità end-to-end, ma presentano alcuni svantaggi di cui dovresti essere a conoscenza:

  • Senza un test isolato per confermare solo ciò che si sta rompendo, un test "cross-unit" fallito richiederà una ulteriore risoluzione dei problemi per determinare cosa c'è di sbagliato nel tuo codice
  • Affidarsi troppo ai test cross-unit può farti uscire dalla mentalità del contratto che dovresti sempre avere quando scrivi un codice orientato agli oggetti SOLID. I test isolati di solito hanno senso perché le tue unità dovrebbero eseguire solo un'azione di base.
  • Il test end-to-end è auspicabile ma può essere pericoloso se un test richiede di scrivere su un database o eseguire un'azione che non si vorrebbe eseguire in un ambiente di produzione. Questa è una delle molte ragioni per cui i mocking framework come Mockito sono così popolari perché ti permettono di fingere un oggetto e di simulare un test end-to-end senza effettivamente cambiare qualcosa che non dovresti.

Alla fine della giornata vuoi avere alcuni di entrambi .. molti test che catturano funzionalità di basso livello e alcune di quelle test end-to-end. Concentrati sul motivo per cui stai scrivendo test in primo luogo. Sono lì per darti la certezza che il tuo codice sta funzionando nel modo in cui te lo aspetti. Qualunque cosa tu debba fare per realizzarlo va bene.

Il fatto triste ma vero è che se stai usando test automatici, allora hai già un vantaggio su molti sviluppatori. Buone pratiche di prova sono la prima cosa da far scivolare quando un team di sviluppo deve affrontare scadenze difficili. Quindi, fintanto che rimani fedele alle tue pistole e scrivi test unitari che riflettono in maniera significativa il modo in cui il tuo codice dovrebbe essere eseguito, non sarei ossessionato da quanto "puri" siano i tuoi test.

    
risposta data 06.09.2016 - 19:01
fonte
4

Un problema con l'utilizzo di altri metodi di oggetti per testare una condizione è che ti mancheranno errori che si annullano a vicenda. Ancora più importante, ti stai perdendo il dolore di un'implementazione difficile del test e quel dolore ti sta insegnando qualcosa sul tuo codice sottostante. I test dolorosi ora indicano una manutenzione dolorosa in seguito.

Riduci le unità, dividi classe, refactoring e riprogetta finché non è più facile testare senza reimplementando il resto del tuo oggetto. Ottieni aiuto se pensi che il tuo codice o test non possano essere ulteriormente semplificati. Prova a pensare a un collega che sembra sempre fortunato e ricevere compiti facili da testare in modo pulito. Chiedigli un aiuto, perché non è una fortuna.

    
risposta data 06.09.2016 - 21:10
fonte
1

Se l'unità di funzionalità testata è "i dati sono memorizzati in modo persistente e recuperabile", allora avrei il test di unità test che - memorizza in un vero database, distrugge qualsiasi oggetto contenente riferimenti al database, quindi chiama il codice per recuperare gli oggetti.

Verificare se un record è stato creato nel database sembra riguardare i dettagli dell'implementazione del meccanismo di archiviazione piuttosto che testare l'unità di funzionalità esposta al resto del sistema.

Potresti voler collegare il test per migliorare le prestazioni usando un database fittizio, ma ho avuto degli appaltatori che hanno fatto esattamente questo e sono usciti alla fine del loro contratto con un sistema che ha superato tali test, ma in realtà non ha archiviato nulla nel database tra riavvii del sistema.

Puoi discutere se 'unità' nel test unitario denota una 'unità di codice' o 'unità di funzionalità', funzionalità forse creata da molte unità di codice in concerto. Non trovo questa distinzione utile: le domande che terrei in mente sono "il test ti dice qualcosa sul fatto che il sistema fornisca un valore aziendale", e "il test è fragile se l'implementazione cambia?" Test come quello descritto sono utili quando si esegue il TDDing del sistema - non si è ancora scritto "get object from database record", quindi non è possibile testare l'intera unità di funzionalità - ma sono fragili rispetto ai cambiamenti di implementazione, quindi rimuovili una volta che l'intera operazione è testabile.

    
risposta data 07.09.2016 - 10:14
fonte
1

Lo spirito è corretto.

Idealmente, in un test unitario , stai testando un'unità (un singolo metodo o una piccola classe).

Idealmente, spegneresti l'intero sistema di database. Ad esempio, eseguirai il tuo metodo in un ambiente simulato e assicurati che invochi le API DB corrette nell'ordine corretto. Esegui in modo esplicito, non , il test del DB quando testi uno dei tuoi metodi.

I vantaggi sono molti. Soprattutto, i test diventano accecanti velocemente perché non è necessario preoccuparsi di configurare un ambiente DB corretto e ripristinarlo in seguito.

Ovviamente, questo è un obiettivo elevato in qualsiasi progetto software che non lo faccia fin dall'inizio. Ma è lo spirito del test unit .

Nota che ci sono altri test, come test delle caratteristiche, test comportamentali, ecc. che sono diversi da questo approccio - non confondere "test" con "test delle unità".

    
risposta data 07.09.2016 - 11:53
fonte
1

C'è almeno un motivo molto importante non per utilizzare l'accesso diretto al database nei test dell'unità per il codice aziendale che interagisce con il database: se si modifica l'implementazione del database, è necessario riscriverlo tutti quei test unitari. Rinominare una colonna, per il tuo codice devi solo modificare una singola riga nella definizione del tuo mapper dei dati. Se non utilizzi il tuo programma di mappatura dei dati durante i test, tuttavia, scoprirai anche che devi modificare ogni singolo test dell'unità che fa riferimento a questa colonna . Ciò potrebbe trasformarsi in una straordinaria quantità di lavoro, in particolare per modifiche più complesse che sono meno suscettibili di ricerca e sostituzione.

Inoltre, usare il tuo mapper dei dati come un meccanismo astratto invece di parlare direttamente al database rende più facile rimuovere completamente la dipendenza dal database , che potrebbe non essere rilevante ora, ma quando si ottiene fino a migliaia di test unitari e tutti stanno colpendo un database, sarete grati di poter facilmente refactoring per rimuovere tale dipendenza perché la vostra suite di test scenderà da pochi minuti per eseguire a prendere secondi, che può avere un enorme beneficio per la tua produttività.

La tua domanda indica che stai pensando lungo le linee giuste. Come sospetti, i test unitari sono anche codice. È altrettanto importante renderli mantenibili e facili da adattare alle modifiche future come per il resto della base di codice. Cerca di mantenerli leggibili e di eliminare la duplicazione, e avrai una suite di test molto migliore. E una buona suite di test è quella che viene utilizzata. E una suite di test che viene utilizzata aiuta a trovare errori, mentre quella che non viene utilizzata è semplicemente inutile.

    
risposta data 08.09.2016 - 19:11
fonte
1

La migliore lezione che ho imparato imparando i test unitari e di integrazione è quella di non testare metodi, ma comportamenti di test. In altre parole, cosa fa questo oggetto?

Quando lo guardo in questo modo, un metodo che persiste i dati e un altro metodo che lo legge torna a essere testabile. Se stai testando i metodi in modo specifico, ovviamente, si finisce con un test per ogni metodo in isolamento, come ad esempio:

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

Questo problema si verifica a causa della prospettiva dei metodi di test. Non testare metodi. Comportamenti di prova Qual è il comportamento della classe di WidgetDao? Persiste i widget. Ok, come si verifica che permangano i widget? Bene, qual è la definizione di persistenza? Significa che quando lo scrivi, puoi leggerlo di nuovo. Quindi leggere + scrivere insieme diventa un test e, a mio parere, un test più significativo.

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

È un test logico, coeso, affidabile e, a mio parere, significativo.

Le altre risposte si concentrano su quanto sia importante l'isolamento, il dibattito pragmatico rispetto a quello realistico e se un test unitario possa o meno interrogare un database. Quelli in realtà non rispondono alla domanda.

Per verificare se è possibile archiviare qualcosa, è necessario memorizzarlo e quindi leggerlo di nuovo. Non puoi testare se qualcosa è stato archiviato se non ti è permesso leggerlo. Non testare la memorizzazione dei dati separatamente dal recupero dei dati. Finirai con test che non ti dicono nulla.

    
risposta data 08.09.2016 - 22:54
fonte
0

Un esempio di un caso di errore che dovresti prefet catturare, è che l'oggetto in prova utilizza un livello di memorizzazione nella cache, ma non riesce a mantenere i dati come richiesto. Quindi, se interroghi l'oggetto, dirai "sì, ho il nuovo nome e indirizzo", ma vuoi che il test fallisca perché non ha effettivamente fatto ciò che avrebbe dovuto.

In alternativa (e trascurando la violazione di responsabilità singola), supponiamo che sia necessario mantenere una versione con codifica UTF-8 della stringa in un campo orientato ai byte, ma in realtà persiste Shift JIS. Qualche altro componente leggerà il database e si aspetta di vedere UTF-8, quindi il requisito. Quindi il round-trip attraverso questo oggetto riporterà il nome e l'indirizzo corretti perché verrà convertito da Shift JIS, ma l'errore non viene rilevato dal test. Si spera che venga rilevato da alcuni test di integrazione successivi, ma l'intero punto dei test unitari è quello di individuare i problemi in anticipo e sapere esattamente quale componente è responsabile.

If one of them is not doing what it's supposed to, then its own test case will fail and we can fix it and run the test battery again.

Non puoi supporre questo, perché se non stai attento scrivi una serie di test reciprocamente dipendenti. Il "salva?" test chiama il metodo di salvataggio che sta testando e quindi il metodo di caricamento per confermarlo. Il "carica?" test chiama il metodo di salvataggio per configurare il dispositivo di prova e quindi il metodo di caricamento che sta testando per verificare il risultato. Entrambi i test si basano sulla correttezza del metodo che non testano, il che significa che nessuno dei due test effettivamente la correttezza del metodo che sta testando.

L'indizio che ci sia un problema qui, è che due test che presumibilmente stanno testando unità differenti in realtà fanno la stessa cosa . Entrambi chiamano un setter seguito da un getter, quindi controllano che il risultato sia il valore originale. Ma volevi testare che il setter persista sui dati, non che la coppia setter / getter lavori insieme. Quindi sai che qualcosa non va, devi solo capire cosa e correggere i test.

Se il tuo codice è ben progettato per il test delle unità, allora ci sono almeno due modi per testare se i dati sono stati effettivamente mantenuti correttamente dal metodo sotto test:

  • prendi in giro l'interfaccia del database e chiedi al tuo mock di registrare il fatto che sono state chiamate le funzioni appropriate, con i valori attesi. Questo verifica che il metodo faccia quello che dovrebbe, ed è il test unitario classico.

  • passa a un vero database con esattamente la stessa intenzione , per registrare se i dati sono stati persi o meno correttamente. Ma piuttosto che avere una funzione derisoria che dice semplicemente "sì, ho i dati giusti", il tuo test ritorna direttamente dal database e conferma che è corretto. Questo potrebbe non essere il test più puro possibile, perché un intero motore di database è una grande cosa da usare per scrivere un finto glorificato, con più possibilità di trascurare qualche sottigliezza che fa passare un test anche se qualcosa è sbagliato (quindi per esempio non dovrei usare la stessa connessione al database per leggere come è stato usato per scrivere, perché potrei vedere una transazione senza commit). Ma mette alla prova la cosa giusta, e almeno sai che precisamente implementa l'intera interfaccia del database senza dover scrivere alcun codice di simulazione!

Quindi è un semplice dettaglio dell'implementazione del test se leggo i dati dal database di test da JDBC o se sto prendendo in giro il database. In ogni caso, il punto è che posso testare l'unità isolandola meglio di me se permetto che si possa cospirare con altri metodi errati sulla stessa classe per guardare bene anche quando qualcosa non va. Quindi voglio usare qualsiasi mezzo conveniente per verificare che i dati corretti siano persistenti, diversi da fidati del componente il cui metodo sto testando.

Se il tuo codice è non ben progettato per il test delle unità, allora potresti non avere scelta, perché l'oggetto il cui metodo vuoi testare potrebbe non accettare il database come dipendenza iniettata. Nel qual caso la discussione sul modo migliore per isolare l'unità in prova, cambia in una discussione su quanto vicino è possibile arrivare ad isolare l'unità sotto test. La conclusione è la stessa, però. Se riesci a evitare cospirazioni tra unità difettose, lo fai, in base al tempo disponibile e a qualsiasi altra cosa tu pensi che sarebbe più efficace nel trovare errori nel codice.

    
risposta data 07.09.2016 - 16:17
fonte
0

Questo è il modo in cui mi piace farlo. Questa è solo una scelta personale in quanto ciò non influirà sul risultato del prodotto, ma solo sul modo di produrlo.

Questo dipende dalle possibilità di poter aggiungere valori fittizi nel tuo database senza l'applicazione che stai testando.

Supponiamo che tu abbia due test: A - lettura del database B - inserire nel database (dipende da A)

Se A pass, allora sei sicuro che il risultato di B dipenderà dalla parte testata in B e non dalle dipendenze. Se A fallisce, puoi avere un falso negativo per B. L'errore potrebbe essere in B o in A. Non puoi saperlo con certezza finché A non ritorna un successo.

Non proverei a scrivere alcune query in un test unitario poiché non dovresti essere in grado di conoscere la struttura del DB dietro l'applicazione (o potrebbe cambiare). Significa che il tuo caso di test potrebbe fallire a causa del tuo stesso codice. A meno che non si scriva un test per il test, ma chi testerà il test per il test ...

    
risposta data 08.09.2016 - 14:17
fonte
-1

Alla fine, si tratta di ciò che si desidera testare.

A volte basta testare l'interfaccia fornita della classe (ad esempio, il record viene creato e memorizzato e può essere recuperato in un secondo momento, magari dopo un "riavvio"). In questo caso va bene (e di solito una buona idea) utilizzare i metodi forniti per il test. Ciò consente di riutilizzare i test, ad esempio se si desidera modificare il meccanismo di memorizzazione (database diverso, database in memoria per il test, ...).

Note that this means you only really care that the class is adhering to its contract (creating, storing and retrieving records in this case), not how it achieves this. If you create your unit tests smartly (against an interface, not a class directly), you can test different implementations, as they also have to adhere to the contract of the interface.

D'altra parte, in alcuni casi devi verificare effettivamente come vengono perse le cose (forse qualche altro processo si basa sul fatto che i dati siano nel formato giusto in quel database?). Ora non puoi fare affidamento solo sul recupero del record corretto dalla classe, ma devi verificare che sia correttamente archiviato nel database. E il modo più diretto per farlo è interrogare il database stesso.

Now you don't only care about the class adhering to its contract (create, store and retrieve), but also how it achieves this (since that persisted data needs to adhere to another interface). However, now the tests are dependent on both the class interface and the storage format, so it will be hard to reuse them fully. (Some might call these kinds of tests "integration tests".)

    
risposta data 06.09.2016 - 23:23
fonte

Leggi altre domande sui tag