Comportamenti di test unitari senza abbinamento ai dettagli di implementazione

10

Nel suo talk TDD, dove è andato tutto storto , Ian Cooper spinge l'intento originale di Kent Beck dietro i test unitari in TDD (per testare comportamenti, non i metodi delle classi in particolare) e argomenta per evitare di abbinare i test all'implementazione.

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

    
posta Andy Hunt 28.03.2014 - 09:46
fonte

2 risposte

7

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

Tuttavia, questa non è una brutta cosa. Le interfacce di confine tra l'applicazione e le sue dipendenze esterne sono non i dettagli di implementazione , infatti, sono definite nell'architettura del tuo sistema; il che significa che un tale limite non è suscettibile di cambiamenti (o se deve, sarebbe il tipo di cambiamento meno frequente). Pertanto, l'accoppiamento dei test a un'interfaccia repository non dovrebbe causare troppi problemi (se lo fa, considera se l'interfaccia non stia rubando responsabilità dall'applicazione).

Ora, considera solo le regole aziendali di un'applicazione, disaccoppiata dall'interfaccia utente, dai database e da altri servizi esterni. Questo è dove dovresti essere libero di cambiare sia la struttura che il comportamento del codice. È qui che i test di accoppiamento e i dettagli di implementazione ti costringono a modificare più codice di test rispetto al codice di produzione, anche quando non vi è alcun cambiamento nel comportamento generale dell'applicazione. È qui che testare State anziché Interaction ci aiuta ad andare più veloce.

PS: Non è mia intenzione dire se il test di Stato o Interazioni è l'unico vero modo di TDD - Credo che si tratti di utilizzare lo strumento giusto per il lavoro giusto.

    
risposta data 29.03.2014 - 15:28
fonte
5

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 ipotizzato per il consiglio sia qualcosa del tipo:

  • stai sviluppando un sistema per gli utenti, non, per esempio, una libreria di utilità o un framework.
  • l'obiettivo del test è riuscire a fornire il più possibile in un budget competitivo.
  • I componenti
  • sono scritti in un unico, maturo, probabilmente tipizzato staticamente, linguaggio come C # / Java.
  • un componente è dell'ordine di 10000-50000 linee; un progetto Maven o VS, un plug-in 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 architettura esagonale
  • una porta del componente è quella in cui si lascia la lingua locale, e il suo tipo di sistema, dietro, passando a http / SQL / XML / byte /...
  • il wrapping di ogni porta sono interfacce tipizzate, in senso Java / C #, che possono avere implementazioni disattivate per cambiare tecnologia.

Quindi testare un componente è il più ampio ambito possibile in cui qualcosa possa ancora essere ragionevolmente definito test unitario. Questo è piuttosto diverso da come alcune persone, specialmente gli accademici, usano il termine. Non è niente come gli esempi nel tipico tutorial sugli strumenti di test unitario. Tuttavia, corrisponde alla sua origine nei test dell'hardware; schede e moduli sono testati unitamente, non cavi e viti. O almeno non costruisci un finto Boeing per testare una vite ...

Estrapolando da ciò e inserendo alcuni dei miei pensieri,

  • Ogni interfaccia sarà o un input, un output o un collaboratore (come un database).
  • tu prova le interfacce di input; chiama i metodi, asserisci i valori di ritorno.
  • tu mock le interfacce di output; verificare che i metodi previsti siano chiamati per un dato test case.
  • tu falso i collaboratori; fornire un'implementazione semplice ma funzionante

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

Un database è generalmente un collaboratore, quindi diventa falso piuttosto che deriso. Ciò sarebbe doloroso da implementare a mano; fortunatamente queste cose esistono già .

Il modello di test di base è una sequenza di operazioni (ad esempio salvataggio e ricarica di un documento); conferma che funziona. Questo è uguale a qualsiasi altro scenario di test; nessuna modifica di implementazione (di lavoro) può causare il fallimento di tale test.

L'eccezione è dove i record del database sono scritti ma mai letti dal sistema sotto test; per esempio. registri di audit o simili. Queste sono uscite, e quindi dovrebbero essere prese in giro. Il modello di prova è una sequenza di operazioni; conferma che l'interfaccia di controllo è stata chiamata con metodi e argomenti come specificato.

Nota che anche qui, purché tu stia utilizzando uno strumento di simulazione di tipo sicuro come mockito , non è possibile rinominare un metodo di interfaccia causa un fallimento del test. Se si utilizza un IDE con i test caricati, verrà refactored insieme al metodo rinominare. Se non lo fai, il test non verrà compilato.

    
risposta data 30.03.2014 - 11:48
fonte

Leggi altre domande sui tag