C'è un punto di unit test che stub e deridono tutto il pubblico?


59

Quando si eseguono i test unitari nel modo "corretto", ovvero reprimendo ogni chiamata pubblica e restituendo valori preimpostati o beffe, mi sento come se non stessi testando nulla. Sto letteralmente guardando il mio codice e creando esempi basati sul flusso di logica attraverso i miei metodi pubblici. E ogni volta che l'implementazione cambia, devo andare a cambiare quei test, ancora una volta, non sentendo davvero che sto realizzando qualcosa di utile (sia a medio che a lungo termine). Faccio anche test di integrazione (compresi i percorsi non felici) e non mi dispiace davvero per i tempi di prova aumentati. Con quelli, mi sento come se stessi effettivamente testando le regressioni, perché hanno catturato molteplici, mentre tutto ciò che i test unitari fanno è mostrarmi che l'implementazione del mio metodo pubblico è cambiata, che già conosco.

I test unitari sono un argomento vasto e mi sento come se non fossi in grado di capire qualcosa qui. Qual è il vantaggio decisivo dei test unitari rispetto ai test di integrazione (escluso il tempo di overhead)?


4
I miei due centesimi: non abusare delle
Juan Mendes,

Risposte:


37

Quando si eseguono i test unitari nel modo "corretto", ovvero reprimendo ogni chiamata pubblica e restituendo valori preimpostati o beffe, mi sento come se non stessi testando nulla. Sto letteralmente guardando il mio codice e creando esempi basati sul flusso di logica attraverso i miei metodi pubblici.

Questo suona come il metodo che stai testando ha bisogno di molte altre istanze di classe (che devi prendere in giro) e chiama diversi metodi da solo.

Questo tipo di codice è davvero difficile da testare l'unità, per i motivi indicati.

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

  1. Classi con l'attuale "logica aziendale". Questi usano poche o nessuna chiamata ad altre classi e sono facili da testare (valore / i in - valore in uscita).
  2. Classi che si interfacciano con sistemi esterni (file, database, ecc.). Questi avvolgono il sistema esterno e forniscono un'interfaccia conveniente 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. In casi più complessi, queste classi potrebbero dover eseguire chiamate da sole, ma chiameranno solo le classi da 2. (e non chiameranno direttamente ad esempio una funzione di database) e le classi da 2. sono facili da deridere (perché solo esporre le parti del sistema avvolto di cui hai bisogno).

Le classi da 2. e 3. di solito non possono essere testate in modo significativo dall'unità (poiché non fanno nulla di utile da sole, 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

Supponi di avere 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 DB, che sono difficili da deridere. In pseudocodice:

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 molta derisione (complessa), che probabilmente si interromperà se il codice o la struttura del DB cambiano.

Dividi

Si divide in tre classi: PriceCalculation, PriceRepository, App.

PriceCalculation esegue solo il calcolo effettivo e riceve i valori di cui ha bisogno. L'app lega tutto insieme:

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

Quel modo:

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

Infine, può risultare che PriceCalculation debba eseguire le proprie chiamate al database. Ad esempio perché solo PriceCalculation conosce quali dati sono necessari, quindi non possono essere recuperati in anticipo dall'app. Quindi puoi passargli un'istanza di PriceRepository (o qualche altra classe di repository), su misura per le esigenze di PriceCalculation. Questa classe dovrà quindi essere derisa, ma sarà semplice, perché l'interfaccia di PriceRepository è semplice, ad es PriceRepository.getPrice(articleNo, contractType). Ancora più importante, l'interfaccia di PriceRepository isola PriceCalculation dal database, quindi è improbabile che le modifiche allo schema del database o all'organizzazione dei dati cambino la sua interfaccia e, quindi, rompano le simulazioni.


5
Pensavo di essere solo a non vedere il punto nell'unità testare tutto, grazie
enthrops

4
Non sono d'accordo quando dici che le classi di tipo 3 sono poche, penso che la maggior parte del mio codice sia di tipo 3 e non ci sia quasi alcuna logica aziendale. Questo è ciò che intendo: stackoverflow.com/questions/38496185/…
Rodrigo Ruiz,

27

Qual è il vantaggio decisivo dei test unitari rispetto ai test di integrazione?

Questa è una falsa dicotomia.

I test unitari e i test di integrazione hanno due scopi simili, ma diversi. Lo scopo del test unitario è assicurarsi che i metodi funzionino. In termini pratici, i test unitari assicurano che il codice soddisfi il contratto indicato dai test unitari. Ciò è evidente nel modo in cui sono progettati i test unitari: essi specificano nello specifico cosa dovrebbe fare il codice e affermano che il codice lo fa.

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

Tuttavia, se i test unitari presentano un vantaggio decisivo, è questo: i test unitari sono molto più facili da impostare e richiedono molto meno tempo e sforzi rispetto ai test di integrazione. Se utilizzati 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 testabile 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 derisione elaborata, configurazione complessa e asserzioni difficili. Al massimo livello di integrazione del sistema, immagina di provare a simulare l'interazione umana in un'interfaccia utente. Interi sistemi software sono dedicati a questo tipo di automazione. Ed è l'automazione che stiamo cercando; i test umani non sono ripetibili e non si ridimensionano come i test automatici.

Infine, i test di integrazione non forniscono garanzie sulla copertura del codice. Quante combinazioni di loop di codice, condizioni e rami stai testando con i tuoi test di integrazione? Lo sai davvero? Esistono strumenti che è possibile utilizzare con unit test e metodi in fase di test che indicano la copertura del codice e la complessità ciclomatica del codice. Ma funzionano davvero bene solo a livello di metodo, dove vivono i test unitari.


Se i tuoi test cambiano ogni volta che fai refactoring, questo è un problema diverso. Si suppone che i test unitari riguardino la documentazione di ciò che fa il tuo software, dimostrando che lo fa e quindi dimostrando che lo fa di nuovo quando rifletti l'implementazione sottostante. Se la tua API cambia o hai bisogno che i tuoi metodi cambino in conformità con una modifica nella progettazione del sistema, è quello che dovrebbe succedere. Se sta succedendo molto, considera 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 molto tempo a scrivere unit test per codici banali come

public string SomeProperty { get; set; }

allora dovresti riesaminare il tuo approccio. Il test unitario dovrebbe testare il comportamento e non esiste alcun comportamento nella riga di codice sopra. Tuttavia, hai creato una dipendenza nel tuo codice da qualche parte, dal momento che quella proprietà verrà quasi sicuramente citata altrove nel tuo codice. Invece di farlo, considera la possibilità di scrivere metodi che accettano la proprietà necessaria come parametro:

public string SomeMethod(string someProperty);

Ora il tuo metodo non ha dipendenze da qualcosa al di fuori di sé, ed è ora più testabile, dal momento che è 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 il comportamento effettivo.


2
Sono consapevole del fatto che i test di unità e i test di integrazione hanno obiettivi diversi, tuttavia, non riesco ancora a capire come i test di unità sono utili se si interrompe e si prende in giro tutte le chiamate pubbliche effettuate dai test di unità. Comprenderei "il codice soddisfa il contratto delineato dai test unitari", se non fosse per matrici e beffe; i miei test unitari sono letteralmente riflessi della logica all'interno dei metodi che sto testando. Tu (I) non stai davvero testando nulla, stai solo guardando il tuo codice e 'convertendolo' in test. Per quanto riguarda le difficoltà di automazione e copertura del codice, attualmente sto realizzando Rails, ed entrambi sono ben curati.
entusiasma

2
Se i tuoi test sono solo riflessi della logica del metodo, stai sbagliando. I test unitari dovrebbero essenzialmente consegnare al metodo un valore, accettare un valore di ritorno e fare un'affermazione su quale dovrebbe essere quel valore di ritorno. Non è richiesta alcuna logica per farlo.
Robert Harvey,

2
Ha senso, ma deve comunque bloccare tutte le altre chiamate pubbliche (db, alcuni "globali", come lo stato dell'utente corrente, ecc.) E finire con il test del codice seguendo la logica del metodo.
entusiasma

1
Quindi immagino che i test unitari siano per roba per lo più isolata che conferma il tipo di "set di input -> set di risultati previsti"?
entusiasma

1
La mia esperienza nella creazione di numerosi test unitari e di integrazione (per non parlare degli strumenti avanzati di derisione, test di integrazione e copertura del codice utilizzati da tali test) contraddice la maggior parte delle affermazioni qui riportate: 1) "Lo scopo del test unitario è quello di assicurarsi che il tuo il codice fa quello che si suppone ": lo stesso vale per i test di integrazione (ancor di più); 2) "i test unitari sono molto più facili da impostare": no, non lo sono (abbastanza spesso, sono i test di integrazione che sono più facili); 3) "Se usati correttamente, i test unitari incoraggiano lo sviluppo di un codice" testabile ": lo stesso con i test di integrazione; (continua)
Rogério,

4

I test unitari con simulazioni sono per assicurarsi che l'implementazione della classe sia corretta. Deridi le interfacce pubbliche delle dipendenze del codice che stai 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 in uno degli altri oggetti.

Stai anche testando il comportamento della classe sotto test e non l'implementazione. Rifattorizzando il codice (creando nuovi metodi interni, ecc.) I test unitari non dovrebbero fallire. Ma se stai cambiando ciò che fa il metodo pubblico, allora i test dovrebbero assolutamente 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 la classe dovrebbe avere e quindi scrivi la quantità minima di codice per far passare i test.

Sia i test unitari che i test di integrazione sono utili per garantire la qualità del codice. I test unitari esaminano ogni componente separatamente. 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. Deridendo i componenti che non ho ancora realizzato. Sono anche ottimi per la regressione, in quanto documentano eventuali bug nella logica che ho riscontrato (anche nei test unitari).

AGGIORNARE

La creazione di un test che si assicura solo che i metodi vengano chiamati ha un valore in quanto si sta assicurando che i metodi vengano effettivamente chiamati. Soprattutto se scrivi prima i test, hai una lista di controllo dei metodi che devono accadere. Poiché questo codice è praticamente procedurale, non c'è molto da controllare oltre a quello che vengono chiamati i metodi. Stai proteggendo il codice per eventuali cambiamenti in futuro. Quando è necessario 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 non può mai cambiare o può 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 modificare i test con il codice.


E se un metodo non chiama nulla di privato, allora non ha senso testarlo, giusto?
attira

Se il metodo è privato, non lo testare esplicitamente, dovrebbe essere testato tramite l'interfaccia pubblica. Tutti i metodi pubblici devono essere testati, per garantire che il comportamento sia corretto.
Schleis,

No, voglio dire se un metodo pubblico non chiama nulla di privato, ha senso testarlo?
entusiasma

Sì. Il metodo fa qualcosa no? Quindi dovrebbe essere testato. Dal punto di vista del test non so se sta usando qualcosa di privato. So solo che se fornisco l'ingresso A, dovrei ottenere l'uscita B.
Schleis,

Oh sì, il metodo fa qualcosa e quel qualcosa è chiamato ad altri metodi pubblici (e solo quello). Quindi il modo in cui verificherebbe "correttamente" che stub le chiamate con alcuni valori di ritorno e quindi imposta le aspettative del messaggio. Cosa ESATTAMENTE stai testando in questo caso? Che le chiamate giuste sono fatte? Bene, hai scritto quel metodo e puoi guardarlo e vedere esattamente cosa fa. Penso che il test unitario sia più appropriato per metodi isolati che dovrebbero essere usati come 'input -> output', quindi puoi creare un sacco di esempi e poi fare test di regressione quando fai refactoring.
attira

3

Stavo vivendo una domanda simile - fino a quando non ho scoperto il potere dei test sui componenti. In breve, sono uguali ai test unitari, tranne per il fatto che non si prende in giro per impostazione predefinita ma si utilizzano oggetti reali (idealmente tramite iniezione di dipendenza).

In questo modo, puoi creare rapidamente test affidabili con una buona copertura del codice. Non c'è bisogno di aggiornare le tue beffe per tutto il tempo. Potrebbe essere un po 'meno preciso dei test unitari con simulazioni al 100%, ma il tempo e il denaro risparmiati lo compensano. L'unica cosa di cui hai veramente bisogno per usare beffe o dispositivi sono backend di archiviazione o servizi esterni.

In realtà, l'eccessiva derisione è un anti-schema: gli anti-schemi e le derisioni TDD sono malvagi .


0

Sebbene l'operazione abbia già segnato una risposta, sto solo aggiungendo i miei 2 centesimi qui.

Qual è il vantaggio decisivo dei test unitari rispetto ai test di integrazione (escluso il tempo di overhead)?

E anche in risposta a

Quando si eseguono i test unitari nel modo "corretto", ovvero reprimendo ogni chiamata pubblica e restituendo valori preimpostati o beffe, mi sento come se non stessi testando nulla.

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

I test unitari funzionano ma ci sono ancora bug?

dalla mia piccola esperienza in test suite, ho capito che i test unitari devono sempre testare la funzionalità di base del metodo di base di una classe. A mio avviso, ogni metodo pubblico, privato o interno merita di avere un test unitario dedicato. Anche nella mia recente esperienza avevo un metodo pubblico che chiamava altri piccoli metodi privati. Quindi, c'erano due approcci:

  1. non creare un test unit (s) per il metodo privato.
  2. crea un Unit Test (s) per un metodo privato.

Se pensi logicamente, il punto di avere un metodo privato è: il metodo pubblico principale sta diventando troppo grande o disordinato. Al fine di risolvere questo saggio refactoring e creare piccoli pezzi di codice che meritano di essere metodi privati ​​separati che a loro volta rendono il tuo metodo pubblico principale meno voluminoso. Rifletti tenendo presente che questo metodo privato potrebbe essere riutilizzato in seguito. Ci possono essere casi in cui non esistono altri metodi pubblici a seconda di quel metodo privato, ma chissà del 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 unitari e sarebbero stati complicati, dal momento che avevi un numero di test unitari per ciascun ramo del metodo pubblico e del metodo privato.

Se avessi scelto l'approccio 2: il codice scritto per i test unitari sarebbe relativamente inferiore e sarebbe molto più facile testarlo.


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 , essi tendono ad essere esaustivi e più di alto livello. Ti diranno che, dato un contributo, tutte le tue lezioni dovrebbero giungere a questa conclusione finale. Per capire di più sull'utilità dei test di integrazione, consulta il link menzionato.

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.