2009-03-03 2 views
9

C#, nUnit e Rhino Mock, se risulta essere applicabile.TDD e DI: iniezioni di dipendenza che diventano ingombranti

La mia ricerca con TDD continua mentre tento di avvolgere i test attorno a una funzione complicata. Diciamo che sto codificando un modulo che, una volta salvato, deve anche salvare gli oggetti dipendenti all'interno del modulo ... risposte per formare domande, allegati se disponibili e voci di "registro" (come "blahblah ha aggiornato il modulo." O "blahblah ha allegato un file."). Questa funzione di salvataggio spegne anche le e-mail a varie persone a seconda di come lo stato del modulo è cambiato durante la funzione di salvataggio.

Ciò significa che per testare completamente la funzione di salvataggio del modulo con tutte le sue dipendenze, devo iniettare cinque o sei fornitori di dati per testare questa unica funzione e assicurarsi che tutto si sia sparato nel modo giusto e nell'ordine. Questo è ingombrante quando si scrivono i costruttori multipli concatenati per l'oggetto modulo per inserire i provider derisi. Penso che mi manchi qualcosa, sia nel modo di refactoring o semplicemente un modo migliore per impostare i fornitori di dati derisi.

Devo studiare ulteriormente i metodi di refactoring per vedere come questa funzione può essere semplificata? Come suona lo schema dell'osservatore, in modo che gli oggetti dipendenti rilevino quando il modulo genitore viene salvato e si gestiscono da soli? So che la gente dice di dividere la funzione in modo che possa essere testata ... nel senso che metto alla prova le singole funzioni di salvataggio di ciascun oggetto dipendente, ma non la funzione di salvataggio del modulo stesso, che stabilisce come ciascuno si deve salvare nel primo posto?

+0

sarebbe d'aiuto nel suggerire miglioramenti se si desidera mostrare il vostro codice. –

risposta

7

Utilizzare un contenitore AutoMocking. Ce n'è uno scritto per RhinoMocks.

Immagina di avere una classe con molte dipendenze iniettate tramite l'iniezione del costruttore.Ecco come si presenta per configurarlo con RhinoMocks, senza contenitore AutoMocking:

private MockRepository _mocks; 
private BroadcastListViewPresenter _presenter; 
private IBroadcastListView _view; 
private IAddNewBroadcastEventBroker _addNewBroadcastEventBroker; 
private IBroadcastService _broadcastService; 
private IChannelService _channelService; 
private IDeviceService _deviceService; 
private IDialogFactory _dialogFactory; 
private IMessageBoxService _messageBoxService; 
private ITouchScreenService _touchScreenService; 
private IDeviceBroadcastFactory _deviceBroadcastFactory; 
private IFileBroadcastFactory _fileBroadcastFactory; 
private IBroadcastServiceCallback _broadcastServiceCallback; 
private IChannelServiceCallback _channelServiceCallback; 

[SetUp] 
public void SetUp() 
{ 
    _mocks = new MockRepository(); 
    _view = _mocks.DynamicMock<IBroadcastListView>(); 

    _addNewBroadcastEventBroker = _mocks.DynamicMock<IAddNewBroadcastEventBroker>(); 

    _broadcastService = _mocks.DynamicMock<IBroadcastService>(); 
    _channelService = _mocks.DynamicMock<IChannelService>(); 
    _deviceService = _mocks.DynamicMock<IDeviceService>(); 
    _dialogFactory = _mocks.DynamicMock<IDialogFactory>(); 
    _messageBoxService = _mocks.DynamicMock<IMessageBoxService>(); 
    _touchScreenService = _mocks.DynamicMock<ITouchScreenService>(); 
    _deviceBroadcastFactory = _mocks.DynamicMock<IDeviceBroadcastFactory>(); 
    _fileBroadcastFactory = _mocks.DynamicMock<IFileBroadcastFactory>(); 
    _broadcastServiceCallback = _mocks.DynamicMock<IBroadcastServiceCallback>(); 
    _channelServiceCallback = _mocks.DynamicMock<IChannelServiceCallback>(); 


    _presenter = new BroadcastListViewPresenter(
     _addNewBroadcastEventBroker, 
     _broadcastService, 
     _channelService, 
     _deviceService, 
     _dialogFactory, 
     _messageBoxService, 
     _touchScreenService, 
     _deviceBroadcastFactory, 
     _fileBroadcastFactory, 
     _broadcastServiceCallback, 
     _channelServiceCallback); 

    _presenter.View = _view; 
} 

Ora, ecco la stessa cosa con un contenitore AutoMocking:

private MockRepository _mocks; 
private AutoMockingContainer _container; 
private BroadcastListViewPresenter _presenter; 
private IBroadcastListView _view; 

[SetUp] 
public void SetUp() 
{ 

    _mocks = new MockRepository(); 
    _container = new AutoMockingContainer(_mocks); 
    _container.Initialize(); 

    _view = _mocks.DynamicMock<IBroadcastListView>(); 
    _presenter = _container.Create<BroadcastListViewPresenter>(); 
    _presenter.View = _view; 

} 

Più facile, vero?

Il contenitore AutoMocking crea automaticamente dei mock per ogni dipendenza nel costruttore, e li è possibile accedere per la prova in questo modo:

using (_mocks.Record()) 
    { 
     _container.Get<IChannelService>().Expect(cs => cs.ChannelIsBroadcasting(channel)).Return(false); 
     _container.Get<IBroadcastService>().Expect(bs => bs.Start(8)); 
    } 

Speranza che aiuta. So che la mia vita di test è stata resa molto più semplice con l'avvento del contenitore AutoMocking.

+0

Questo approccio nasconde solo la complessità, non vi allevia.Il problema di root qui è nel codice che viene testato, non il codice di test stesso. –

+0

Questa è una cosa ragionevole da fare. Basta fare attenzione ai test che stabiliscono aspettative su più servizi contemporaneamente. Ogni test dovrebbe in genere impostare le aspettative rispetto a un solo servizio alla volta. È bello avere uno strumento per mettere in attesa matrici per il resto. –

5

Hai ragione che può essere ingombrante.

Il proponente della metodologia di derisione farebbe notare che il codice non è stato scritto correttamente. Cioè, non dovresti costruire oggetti dipendenti all'interno di questo metodo. Piuttosto, le API di iniezione dovrebbero avere funzioni che creano gli oggetti appropriati.

Come per il mocking di 6 oggetti diversi, è vero. Tuttavia, se anche i test dei sistemi sono stati testati unitariamente, tali oggetti dovrebbero già disporre di un'infrastruttura di simulazione che è possibile utilizzare.

Infine, utilizzare una struttura di simulazione che fa parte del lavoro per voi.

1

Il DI di fabbrica non è l'unico modo per eseguire DI. Dal momento che stai utilizzando C#, se il tuo costruttore non svolge un lavoro significativo, puoi utilizzare Proprietà DI. Ciò semplifica notevolmente le cose in termini di costruttori dell'oggetto a scapito della complessità della tua funzione. La tua funzione deve verificare la nullità delle proprietà dipendenti e lanciare InvalidOperation se sono nulle, prima che inizi il lavoro.

+0

non sono d'accordo, il che rende la proprietà basata non semplifica, nasconde semplicemente la complessità. – eglasius

+0

Beh, per semplicità, voglio dire che ti permette di trasferire la complessità da una posizione all'altra. Ciò potrebbe in realtà semplificare le cose in termini di test o di altri aspetti del sistema, consentendo di gestire porzioni più semplici in blocchi più piccoli. – Randolpho

0

Quando è difficile testare qualcosa, di solito è sintomo della qualità del codice, che il codice non è verificabile (menzionato in this podcast, IIRC). La raccomandazione è di rifattorizzare il codice in modo che il codice sia facile da testare. Alcune euristiche per decidere come suddividere il codice in classi sono SRP and OCP. Per istruzioni più specifiche, sarebbe necessario vedere il codice in questione.

15

Per prima cosa, se si sta seguendo TDD, non si eseguono i test attorno a una funzione complicata. Si avvolge la funzione attorno ai test. In realtà, anche quello non è giusto. Si intrecciano i test e le funzioni, scrivendo entrambi quasi allo stesso tempo, con i test un po 'più avanti rispetto alle funzioni. Vedi The Three Laws of TDD.

Quando si seguono queste tre leggi e si è diligenti nel rifattorizzare, non si finisce mai con "una funzione complicata". Piuttosto si finisce con molte, testate, semplici funzioni.

Ora, al punto. Se hai già una "funzione complicata" e desideri eseguire dei test intorno a questo, dovresti:

  1. Aggiungi i tuoi mock esplicitamente, anziché tramite DI. (ad esempio qualcosa di orribile come un flag "test" e un'istruzione "if" che seleziona i mock invece degli oggetti reali).
  2. Scrivere alcuni test per coprire le operazioni di base del componente.
  3. Refactatore senza pietà, suddividendo la complicata funzione in tante piccole e semplici funzioni, mentre esegui i test con il cobbled insieme il più spesso possibile.
  4. Spingere il flag 'test' il più in alto possibile. Come refactoring, trasferisci le tue origini dati alle piccole e semplici funzioni. Non lasciare che il flag 'test' infetti nessuno tranne la funzione più in alto.
  5. Test di riscrittura. Come refactoring, riscrivi quanti più test possibili per chiamare le semplici funzioni piuttosto che la grande funzione di primo livello. Puoi passare i tuoi mock alle semplici funzioni dei tuoi test.
  6. Sbarazzarsi del flag 'test' e determinare la quantità di DI di cui si ha realmente bisogno. Dato che i test sono scritti ai livelli inferiori che possono inserire dei mock attraverso gli strumenti, probabilmente non è più necessario prendere in giro molte fonti di dati al livello più alto.

Se, dopo tutto, il DI è ancora macchinoso, quindi pensa di iniettare un singolo oggetto che contiene riferimenti a tutte le tue fonti di dati. È sempre più facile iniettare una cosa piuttosto che molte.

+0

@ zio Bob. hai menzionato esattamente quello che ho fatto, nelle linee iniziali. – vijaysylvester

+1

Per favore, nessun dio obietta. Ho passato troppa parte della mia vita a ripulire oggetti che dipendono dalla madre e che riducono la modularità facendo dipendere tutto il codice da tutte le dipendenze. –

+0

@ Bob Bob, grazie. Le prime 2 frasi mi hanno colpito. –

5

Non ho il tuo codice, ma la mia prima reazione è che il tuo test sta cercando di dirti che il tuo oggetto ha troppi collaboratori. In casi come questo, trovo sempre che c'è un costrutto mancante che dovrebbe essere impacchettato in una struttura di livello superiore. L'utilizzo di un contenitore per l'automocking è solo un mormorio del feedback che si ottiene dai test. Vedi http://www.mockobjects.com/2007/04/test-smell-bloated-constructor.html per una discussione più lunga.

4

In questo contesto, di solito trovo dichiarazioni sulla falsariga di "questo indica che il tuo oggetto ha troppe dipendenze" o "il tuo oggetto ha troppi collaboratori" per essere un'affermazione piuttosto pretestuosa. Ovviamente un controller MVC o un modulo chiamerà molti servizi e oggetti diversi per adempiere ai propri doveri; dopotutto, si trova nello strato superiore dell'applicazione. È possibile sfumare alcune di queste dipendenze insieme in oggetti di livello superiore (ad esempio, un ShippingMethodRepository e un TransitTimeCalculator vengono combinati in un oggetto ShippingRateFinder), ma questo va solo oltre, specialmente per questi oggetti orientati alla presentazione di livello superiore. Questo è un oggetto in meno da deridere, ma hai appena offuscato le dipendenze reali tramite uno strato di riferimento indiretto, in realtà non le hai rimosse.

Un consiglio blasfemo è quello di dire che se si è dipendenti dall'iniettare un oggetto e creare un'interfaccia per esso è molto improbabile che cambi mai (farai davvero un drop in un nuovo MessageBoxService mentre cambi il tuo codice? ?), quindi non preoccuparti. Tale dipendenza è parte del comportamento previsto dell'oggetto e dovresti semplicemente testarli insieme poiché il test di integrazione è il vero valore aziendale.

L'altro consiglio blasfemo è che di solito vedo poca utilità in unità di controllo MVC controller o Windows Form. Ogni volta che vedo qualcuno deridere HttpContext e test per vedere se un cookie è stato impostato, voglio urlare. A chi importa se l'AccountController imposta un cookie? Io non. Il cookie non ha nulla a che fare con il trattamento del controller come una scatola nera; un test di integrazione è ciò che è necessario per testare la sua funzionalità (hmm, una chiamata a PrivilegedArea() fallita dopo Login() nel test di integrazione). In questo modo, si evita di invalidare un milione di test unitari inutili se il formato del cookie di accesso cambia mai.

Salvare i test di unità per il modello a oggetti, salvare i test di integrazione per il livello di presentazione ed evitare gli oggetti mock quando possibile. Se prendersi gioco di una particolare dipendenza è difficile, è il momento di essere pragmatici: basta non fare il test unitario e scrivere invece un test di integrazione e smettere di sprecare il tuo tempo.

3

La semplice risposta è che il codice che si sta tentando di testare sta facendo troppo. Penso che attenersi al Single Responsibility Principle potrebbe aiutare.

Il metodo di pulsante Salva deve contenere solo chiamate di livello superiore per delegare oggetti ad altri oggetti. Questi oggetti possono quindi essere astratti tramite interfacce. Quindi, quando si esegue il test del metodo del pulsante Salva, si verifica solo l'interazione con gli oggetti simulati.

Il passaggio successivo consiste nel scrivere test su queste classi di livello inferiore, ma la cosa dovrebbe essere più semplice poiché le si verifica solo separatamente. Se hai bisogno di un codice di setup di test complesso, questo è un buon indicatore di un cattivo design (o di un approccio di test negativo).

Letture consigliate:

  1. Clean Code: A Handbook of Agile Software Craftsmanship
  2. Google's guide to writing testable code