C'è un punto per i test unitari che mozzano e deridono tutto ciò che è pubblico?

56

Quando si eseguono i test unitari in modo "corretto", vale a dire, stubando ogni chiamata pubblica e restituendo valori preimpostati o mock, mi sembra di non provare nulla. Sto letteralmente guardando il mio codice e creando esempi basati sul flusso della logica attraverso i miei metodi pubblici. E ogni volta che l'implementazione cambia, devo andare e cambiare quei test, ancora una volta, non sentendo davvero che sto realizzando qualcosa di utile (sia a medio che a lungo termine). Faccio anche dei test di integrazione (compresi i percorsi non felici) e non mi preoccupo molto dei tempi di test aumentati. Con quelli, mi sento come se stessimo testando le regressioni, perché sono state catturate più volte, mentre tutti i test unitari mi mostrano che l'implementazione del mio metodo pubblico è cambiata, cosa che già conosco.

Il test delle unità è un argomento vasto e mi sento come se non fossi in grado di capire qualcosa qui. Qual è il vantaggio decisivo del test unitario rispetto al test di integrazione (escluso il tempo di overhead)?

    
posta enthrops 17.05.2013 - 03:11
fonte

5 risposte

35

When doing unit tests the "proper" way, i.e. stubbing every public call and return preset values or mocks, I feel like I'm not actually testing anything. I'm literally looking at my code and creating examples based on the flow of logic through my public methods.

Sembra che il metodo che stai testando richieda diverse altre istanze di classe (che devi prendere in giro) e chiama da solo diversi metodi.

Questo tipo di codice è davvero difficile da testare unitamente, per i motivi che descrivi.

Ciò che ho trovato utile è suddividere tali classi in:

  1. Classi con l'attuale "business logic". Questi usano poche o nessuna chiamata ad altre classi e sono facili da testare (value (s) in - value out).
  2. Classi che si interfacciano con sistemi esterni (file, database, ecc.). Questi avvolgono il sistema esterno e forniscono una comoda interfaccia per le tue esigenze.
  3. Classi che "legano tutto insieme"

Quindi le classi da 1. sono facili da testare, perché accettano solo valori e restituiscono un risultato. Nei casi più complessi, queste classi potrebbero dover effettuare chiamate da sole, ma chiameranno solo le classi da 2. (e non chiamano direttamente, ad esempio, una funzione di database), e le classi da 2. sono facili da deridere (perché solo loro esporre le parti del sistema incartato di cui hai bisogno)

Le classi da 2. e 3. di solito non possono essere significativamente testate in unità (perché non fanno nulla di utile da soli, sono solo codice "colla"). OTOH, queste classi tendono ad essere relativamente semplici (e poche), quindi dovrebbero essere adeguatamente coperte dai test di integrazione.

Un esempio

Una classe

Supponiamo che tu abbia una classe che recupera un prezzo da un database, applica alcuni sconti e quindi aggiorna il database.

Se hai tutto questo in una classe, dovrai chiamare le funzioni di DB, che sono difficili da prendere in giro. In pseudocode:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Tutti e tre i passaggi avranno bisogno dell'accesso al DB, quindi un sacco di (complesse) derisioni, che è probabile che si interrompano se il codice o la struttura del DB cambiano.

Suddividi

Ti sei suddiviso in tre classi: PriceCalculation, PriceRepository, App.

PriceCalculation esegue solo il calcolo effettivo e ottiene i valori necessari. L'app lega tutto insieme:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

In questo modo:

  • PriceCalculation incapsula la "logica aziendale". È facile da testare perché non chiama nulla da solo.
  • PriceRepository può essere testato pseudo-unità impostando un database fittizio e testando le chiamate di lettura e aggiornamento. Ha poca logica, quindi pochi codepath, quindi non hai bisogno di troppi test.
  • L'app non può essere significativamente testata dall'unità, perché è un codice collettivo. Tuttavia, anche questo è molto semplice, quindi i test di integrazione dovrebbero essere sufficienti. Se l'App successiva diventa troppo complessa, crei più classi "business-logic".

Infine, potrebbe risultare che PriceCalculation debba effettuare le proprie chiamate al database. Ad esempio, poiché solo PriceCalculation conosce quali dati sono necessari, quindi non può essere recuperato in anticipo dall'App. Quindi è possibile passarlo un'istanza di PriceRepository (o qualche altra classe di repository), personalizzata in base alle esigenze di PriceCalculation. Questa classe dovrà quindi essere derisa, ma ciò sarà semplice, perché l'interfaccia di PriceRepository è semplice, ad es. %codice%. Ancora più importante, l'interfaccia di PriceRepository isola PriceCalculation dal database, quindi è improbabile che le modifiche allo schema o all'organizzazione dei dati cambino la sua interfaccia e quindi interrompano i mock.

    
risposta data 22.05.2013 - 08:53
fonte
27

What's the decisive advantage of unit testing vs integration testing?

Questa è una falsa dicotomia.

Test di unità e test di integrazione hanno due scopi simili ma diversi. Lo scopo del test unitario è assicurarsi che i tuoi metodi funzionino. In termini pratici, i test unitari assicurano che il codice soddisfi il contratto delineato dai test unitari. Questo è evidente nel modo in cui sono progettati i test unitari: specificano specificamente cosa il codice dovrebbe fare e asserire che il codice lo fa.

I test di integrazione sono diversi. I test di integrazione esercitano l'interazione tra i componenti software. È possibile avere componenti software che superano tutti i loro test e continuano a fallire i test di integrazione perché non interagiscono correttamente.

Tuttavia, se c'è un vantaggio decisivo per i test unitari, è questo: i test unitari sono molto più facili da configurare e richiedono molto meno tempo e impegno rispetto ai test di integrazione. Se usato correttamente, i test unitari incoraggiano lo sviluppo di codice "testabile", il che significa che il risultato finale sarà più affidabile, più facile da capire e più facile da mantenere. Il codice verificabile ha alcune caratteristiche, come un'API coerente, un comportamento ripetibile e restituisce risultati facili da affermare.

I test di integrazione sono più difficili e più costosi, perché spesso hai bisogno di una simulazione elaborata, di una configurazione complessa e di asserzioni difficili. Al più alto livello di integrazione del sistema, immagina di provare a simulare l'interazione umana in un'interfaccia utente. Interi sistemi software sono dedicati a quel tipo di automazione. Ed è l'automazione che cerchiamo; il test umano non è ripetibile e non ha le stesse dimensioni dei test automatizzati.

Infine, i test di integrazione non garantiscono la copertura del codice. Quante combinazioni di loop di codice, condizioni e rami stai testando con i tuoi test di integrazione? Lo sai davvero? Ci sono strumenti che puoi usare con unit test e metodi sotto test che ti diranno quanta copertura di codice hai e cosa è la complessità ciclomatica del tuo codice. Ma funzionano davvero bene solo a livello di metodo, dove i test unitari sono in diretta.

Se i tuoi test cambiano ogni volta che fai refactoring, questo è un altro problema. I test unitari dovrebbero riguardare la documentazione di ciò che fa il software, dimostrando che lo fa, e quindi dimostrare che lo fa di nuovo quando si refactoring l'implementazione sottostante. Se la tua API cambia o hai bisogno che i tuoi metodi cambino in base a un cambiamento nella progettazione del sistema, è ciò che dovrebbe accadere. Se sta accadendo molto, considera prima di scrivere i tuoi test, prima di scrivere il codice. Questo ti costringerà a pensare all'architettura generale e ti consentirà di scrivere codice con l'API già stabilita.

Se passi un sacco di tempo a scrivere test unitari per codice banale come

public string SomeProperty { get; set; }

allora dovresti riesaminare il tuo approccio. Il testing delle unità dovrebbe testare comportamento, e non c'è alcun comportamento nella riga di codice sopra. Tuttavia, hai creato una dipendenza dal tuo codice da qualche parte, dal momento che quella proprietà è quasi certamente indicata altrove nel tuo codice. Invece di farlo, prendi in considerazione la scrittura di metodi che accettano la proprietà necessaria come parametro:

public string SomeMethod(string someProperty);

Ora il tuo metodo non ha alcuna dipendenza da qualcosa al di fuori di se stesso, ed è ora più testabile, poiché è completamente autonomo. Certo, non sarai sempre in grado di farlo, ma sposta il tuo codice nella direzione di essere più testabile, e questa volta stai scrivendo un test unitario per comportamento effettivo.

    
risposta data 17.05.2013 - 05:16
fonte
4

I test unitari con i mock sono per assicurarsi che l'implementazione della classe sia corretta. Si prendono in giro le interfacce pubbliche delle dipendenze del codice che si sta testando. In questo modo hai il controllo su tutto ciò che è esterno alla classe e sei sicuro che un test fallito sia dovuto a qualcosa di interno alla classe e non a uno degli altri oggetti.

Stai anche testando il comportamento della classe sotto test e non l'implementazione. Se si effettua il refactoring del codice (creando nuovi metodi interni, ecc.) I test dell'unità non devono fallire. Ma se stai cambiando il metodo pubblico, allora i test dovrebbero fallire perché hai cambiato il comportamento.

Sembra anche che tu stia scrivendo i test dopo aver scritto il codice, prova invece a scrivere i primi test. Prova a delineare il comportamento che dovrebbe avere la classe e poi scrivi la quantità minima di codice per far passare i test.

Sia i test di unità che i test di integrazione sono utili per garantire la qualità del codice. I test unitari esaminano ciascun componente in isolamento. E i test di integrazione assicurano che tutti i componenti interagiscano correttamente. Voglio avere entrambi i tipi nella mia suite di test.

I test unitari mi hanno aiutato nel mio sviluppo in quanto posso concentrarmi su un pezzo dell'applicazione alla volta. Prendere in giro i componenti che non ho ancora creato. Sono anche ottimi per la regressione, in quanto documentano eventuali errori nella logica che ho trovato (anche nei test unitari).

Aggiorna

Creare un test che si assicuri solo che i metodi siano chiamati ha un valore in quanto ci si assicura che i metodi vengano effettivamente chiamati. Soprattutto se stai scrivendo i tuoi test prima, hai una lista di controllo dei metodi che devono accadere. Dato che questo codice è praticamente procedurale, non hai molto da controllare se non che i metodi vengono chiamati. Stai proteggendo il codice per il cambiamento in futuro. Quando hai bisogno di chiamare un metodo prima dell'altro. O che un metodo viene sempre chiamato anche se il metodo iniziale genera un'eccezione.

Il test per questo metodo potrebbe non cambiare mai o potrebbe cambiare solo quando si modificano i metodi. Perché questa è una brutta cosa? Aiuta a rinforzare usando i test. Se devi correggere un test dopo aver modificato il codice, avrai l'abitudine di cambiare i test con il codice.

    
risposta data 17.05.2013 - 03:32
fonte
2

Stavo vivendo una domanda simile - fino a quando ho scoperto la potenza dei test dei componenti. In breve, sono gli stessi dei test unitari, tranne per il fatto che non si scherza di default ma si usano oggetti reali (idealmente tramite l'integrazione delle dipendenze).

In questo modo, puoi creare rapidamente test robusti con una buona copertura del codice. Non c'è bisogno di aggiornare le tue finte tutte le volte. Potrebbe essere un po 'meno preciso dei test unitari con il 100% di mock, ma il tempo e il denaro risparmiati lo compensano. L'unica cosa di cui hai veramente bisogno di usare mock o fixture sono i backend di archiviazione oi servizi esterni.

In realtà, il mocking eccessivo è un anti-pattern: TDD Anti-Patterns e I mock sono cattivi .

    
risposta data 21.11.2015 - 15:39
fonte
0

Anche se l'op ha già segnato una risposta, sto solo aggiungendo i miei 2 centesimi qui.

What's the decisive advantage of unit testing vs integration testing (excluding the time overhead)?

E anche in risposta a

When doing unit tests the "proper" way, i.e. stubbing every public call and return preset values or mocks, I feel like I'm not actually testing anything.

C'è un utile ma non esattamente quello che l'OP ha chiesto:

I test delle unità funzionano ma ci sono ancora bug?

dalla mia piccola esperienza sulle suite di test, capisco che i Test delle unità devono sempre testare la funzionalità di livello di metodo più basilare di una classe. Secondo me ogni metodo pubblico, privato o interno merita di avere un test unitario dedicato. Anche nella mia recente esperienza ho avuto un metodo pubblico che chiamava altri piccoli metodi privati. Quindi, c'erano due approcci:

  1. non creare un test di unità per metodo privato.
  2. crea un test unitario per un metodo privato.

Se pensi logicamente, il punto di avere un metodo privato è: il metodo pubblico principale sta diventando troppo grande o disordinato. Per risolvere ciò, ragionate saggiamente e create piccoli pezzi di codice che meritano di essere metodi privati separati che a loro volta rendono il vostro principale metodo pubblico meno ingombrante. Si refactoring tenendo presente che questo metodo privato potrebbe essere riutilizzato in seguito. Ci possono essere casi in cui non ci sono altri metodi pubblici che dipendono da quel metodo privato, ma che conoscono il futuro.

Considerando il caso in cui il metodo privato viene riutilizzato da molti altri metodi pubblici.

Quindi, se avessi scelto l'approccio 1: avrei duplicato i test di unità e sarebbero stati complicati, dato che avevi numero di test unitari per ogni ramo del metodo pubblico e metodo privato.

Se avessi scelto l'approccio 2: il codice scritto per i test unitari sarebbe relativamente meno, e sarebbe molto più facile da testare.

Considerando il caso in cui il metodo privato non viene riutilizzato Non ha senso scrivere un test unitario separato per quel metodo.

Per quanto riguarda i Test di integrazione , sono tendenzialmente esaurienti e di alto livello. Ti diranno che, dato un input, tutte le tue lezioni dovrebbero arrivare a questa conclusione finale. Per comprendere meglio l'utilità dei test di integrazione, ti preghiamo di consultare il link indicato.

    
risposta data 07.08.2013 - 14:42
fonte

Leggi altre domande sui tag