Comportamenti di unit test senza accoppiamento ai dettagli di implementazione


16

Nel suo discorso TDD, dove tutto è andato storto , Ian Cooper spinge l'intenzione originale di Kent Beck dietro i test unitari in TDD (per testare comportamenti, non metodi di classi specificamente) e sostiene di evitare di accoppiare i test all'implementazione.

Nel caso di comportamenti come save X to some data sourcein un sistema con un set tipico di servizi e repository, come possiamo testare l'unità del salvataggio di alcuni dati a livello di servizio, attraverso il repository, senza associare il test ai dettagli di implementazione (come chiamare un metodo specifico )? Evitare questo tipo di accoppiamento in realtà non vale lo sforzo / il male in qualche modo?


1
Se vuoi testare che i dati sono stati salvati nel repository, allora il test dovrà effettivamente andare e controllare il repository per vedere se i dati sono lì, giusto? O mi sta sfuggendo qualcosa?

La mia domanda riguardava più di evitare di accoppiare i test a un dettaglio di implementazione come chiamare un metodo specifico nel repository, o davvero se è qualcosa che dovrebbe essere fatto.
Andy Hunt,

Risposte:


8

Il tuo esempio specifico è un caso che di solito devi verificare controllando se è stato chiamato un determinato metodo, perché saving X to data sourcesignifica comunicare con una dipendenza esterna , quindi il comportamento che devi testare è che la comunicazione sta avvenendo come previsto .

Tuttavia, questa non è una brutta cosa. Le interfacce al contorno tra la tua applicazione e le sue dipendenze esterne non sono dettagli di implementazione , infatti sono definiti nell'architettura del tuo sistema; ciò significa che è probabile che un tale confine non cambi (o, se necessario, sarebbe il tipo di cambiamento meno frequente). Pertanto, associare i test a repositoryun'interfaccia non dovrebbe causare troppi problemi (in tal caso, considerare se l'interfaccia non ruba le responsabilità dall'applicazione).

Ora, considera solo le regole di business di un'applicazione, disaccoppiate da UI, database e altri servizi esterni. Questo è dove devi essere libero di cambiare sia la struttura che il comportamento del codice. È qui che i test di accoppiamento e i dettagli di implementazione ti costringeranno a cambiare più codice di test rispetto al codice di produzione, anche quando non vi è alcun cambiamento nel comportamento generale dell'applicazione. Qui è dove i test Stateinvece di Interactionaiutarci ad andare più veloci.

PS: Non è mia intenzione dire se il test da parte dello Stato o delle interazioni sia l'unico vero modo per TDD - credo che si tratti di usare lo strumento giusto per il lavoro giusto.


Quando parli di "comunicare con una dipendenza esterna", ti riferisci alle dipendenze esterne come a quelle esterne all'unità sottoposta a test o a quelle esterne al sistema nel suo insieme?
Andy Hunt,

Per "dipendenza esterna" intendo qualsiasi cosa tu possa essere considerata come un plug-in per la tua applicazione. Per applicazione intendo le regole aziendali, indipendente da ogni tipo di dettaglio, ad esempio quale framework utilizzare per la persistenza o l'interfaccia utente. Penso che lo zio Bob possa spiegarlo meglio, come in questo discorso: youtube.com/watch?v=WpkDN78P884
MichelHenrich

Penso che questo sia l'approccio ideale, come dice il discorso, per testare su una "caratteristica" o "comportamento" e un test per caratteristica o comportamento (o permutazione di uno, ovvero parametri variabili). Tuttavia, se ho 1 test "felice" per una funzione, al fine di fare TDD, ciò significa che avrò un singolo commit gigante (e revisione del codice) per quella funzione, che è una cattiva idea. Come sarebbe evitato? Scrivi una parte di quella funzione come test e tutto il codice ad essa associato, quindi aggiungi in modo incrementale il resto della funzione nei commit successivi?
Giordania,

Mi piacerebbe davvero vedere un esempio reale di test che si stanno accoppiando all'implementazione.
Positivo

7

La mia interpretazione di quel discorso è:

  • componenti di test, non classi.
  • testare i componenti attraverso le loro porte di interfaccia.

Non è indicato nel discorso, ma penso che il contesto presunto per il consiglio sia qualcosa di simile:

  • stai sviluppando un sistema per gli utenti, non, per esempio, una libreria di utilità o un framework.
  • l'obiettivo del test è fornire con successo il più possibile entro un budget competitivo.
  • i componenti sono scritti in un unico linguaggio maturo, probabilmente di tipo statico, come C # / Java.
  • un componente è dell'ordine di 10000-50000 linee; un progetto Maven o VS, plugin OSGI, ecc.
  • i componenti sono scritti da un singolo sviluppatore o da un team strettamente integrato.
  • stai seguendo la terminologia e l'approccio di qualcosa come l' architettura esagonale
  • una porta componente è dove si lascia la lingua locale, e il suo sistema di tipi, dietro, passando a http / SQL / XML / byte / ...
  • il wrapping di ogni porta sono interfacce tipizzate, nel senso Java / C #, che possono avere implementazioni implementate per cambiare tecnologia.

Quindi testare un componente è l'ambito più ampio possibile in cui qualcosa può ancora essere ragionevolmente chiamato unit test. Questo è piuttosto diverso da come alcune persone, specialmente accademici, usano il termine. Non è niente come gli esempi nel tipico tutorial dello strumento di unit test. Tuttavia, corrisponde alla sua origine nei test hardware; schede e moduli sono testati dall'unità, non fili e viti. O almeno non costruisci un finto Boeing per provare una vite ...

Estrapolando da quello e gettando alcuni dei miei pensieri,

  • Ogni interfaccia sarà un input, un output o un collaboratore (come un database).
  • si prova le interfacce di input; chiama i metodi, asserisci i valori di ritorno.
  • si deridere le interfacce di uscita; verificare che vengano chiamati i metodi previsti per un determinato caso di test.
  • si finti i collaboratori; fornire un'implementazione semplice ma funzionante

Se lo fai in modo corretto e pulito, hai a malapena bisogno di uno strumento beffardo; viene utilizzato solo poche volte per sistema.

Un database è generalmente un collaboratore, quindi viene simulato piuttosto che deriso. Ciò sarebbe doloroso da attuare a mano; per fortuna cose del genere esistono già .

Lo schema di prova di base consiste nell'eseguire alcune sequenze di operazioni (ad es. Salvataggio e ricarica di un documento); confermare che funziona. Questo è lo stesso di qualsiasi altro scenario di test; nessun cambiamento di implementazione (funzionante) è suscettibile di causare il fallimento di tale test.

L'eccezione è dove i record del database vengono scritti ma mai letti dal sistema in prova; ad es. registri di controllo o simili. Questi sono output e quindi dovrebbero essere derisi. Lo schema di test prevede alcune sequenze di operazioni; confermare che l'interfaccia di controllo è stata chiamata con metodi e argomenti come specificato.

Si noti che anche qui, a condizione che si stia utilizzando uno strumento di simulazione di tipo sicuro come mockito , la ridenominazione di un metodo di interfaccia non può causare un errore del test. Se si utilizza un IDE con i test caricati, verrà refactored insieme al metodo rinomina. In caso contrario, il test non verrà compilato.


Puoi descrivermi / darmi un esempio concreto di una porta di interfaccia?
Positivo

qual è un esempio di un'interfaccia di output. Puoi essere specifico nel codice? Lo stesso con l'interfaccia di input.
Positivo

Un'interfaccia (in senso Java / C #) avvolge una porta, che può essere qualsiasi cosa che parli al mondo esterno (d / b, socket, http, ....). Un'interfaccia di output è quella che non ha metodi con valori di ritorno che provengono dal mondo esterno tramite la porta, solo eccezioni o equivalenti.
soru,

Un'interfaccia di input è l'opposto, un collaboratore è sia input che output.
soru,

1
Penso che tu stia parlando di un approccio progettuale completamente diverso e di una serie di terminologie rispetto a quella descritta nel video. Ma il 90% delle volte un repository (ovvero un database) è un collaboratore, non un input o un output. E quindi l'interfaccia è un'interfaccia di collaborazione.
soru,

0

Il mio suggerimento è di utilizzare un approccio di test basato sullo stato:

FORNITO Abbiamo il DB di prova in uno stato noto

QUANDO il servizio viene chiamato con argomenti X

POI asserire che il DB è cambiato dal suo stato originale allo stato previsto chiamando i metodi di repository di sola lettura e controllando i loro valori restituiti

In questo modo, non fai affidamento su alcun algoritmo interno del servizio e sei libero di riformattare la sua implementazione senza dover modificare i test.

L'unico accoppiamento qui è la chiamata al metodo di servizio e le chiamate al repository necessarie per leggere i dati dal DB, il che va bene.

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.