Domanda

C #, nUnit e Rhino Mock, se risulta applicabile.

La mia ricerca con TDD continua mentre cerco di avvolgere i test attorno a una funzione complicata. Supponiamo che stia codificando un modulo che, una volta salvato, deve anche salvare oggetti dipendenti all'interno del modulo ... risposte a domande del modulo, allegati se disponibili e " log " voci (come "blahblah ha aggiornato il modulo." o "blahblah ha allegato un file."). Questa funzione di salvataggio attiva anche e-mail a varie persone a seconda di come lo stato del modulo è cambiato durante la funzione di salvataggio.

Questo 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 funzione e assicurarmi che tutto sia stato eseguito correttamente 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 in termini di refactoring o semplicemente un modo migliore per impostare i fornitori di dati derisi.

Dovrei studiare ulteriormente i metodi di refactoring per vedere come questa funzione può essere semplificata? Come suona il modello di osservatore, in modo che gli oggetti dipendenti rilevino quando il modulo genitore viene salvato e si gestiscono? So che le persone dicono di dividere la funzione in modo che possa essere testata ... il che significa che collaudo le singole funzioni di salvataggio di ciascun oggetto dipendente, ma non la funzione di salvataggio del modulo stesso, che determina come ognuno dovrebbe salvarsi nel primo posto?

È stato utile?

Soluzione

Utilizza 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 con RhinoMocks, nessun 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, sì?

Il contenitore AutoMocking crea automaticamente simulazioni per ogni dipendenza nel costruttore e puoi accedervi per test in questo modo:

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

Spero che sia d'aiuto. So che la mia vita di prova è stata molto più semplice con l'avvento del contenitore AutoMocking.

Altri suggerimenti

In primo luogo, se stai seguendo TDD, non avvolgi i test attorno a una funzione complicata. Avvolgi la funzione attorno ai tuoi test. In realtà, anche quello non è giusto. Intreccia test e funzioni, scrivendo entrambi quasi esattamente allo stesso tempo, con i test appena un po 'più avanti delle funzioni. Vedi Le tre leggi del TDD .

Quando segui queste tre leggi e sei diligente nel refactoring, non finisci mai con "una funzione complicata". Piuttosto finisci con molte funzioni testate e semplici.

Ora, passiamo al punto. Se hai già " una funzione complicata " e vuoi avvolgerlo nei test, dovresti:

  1. Aggiungi esplicitamente i tuoi mock, anziché tramite DI. (ad es. qualcosa di orribile come una bandiera "test" e un'istruzione "if" che seleziona le beffe invece degli oggetti reali).
  2. Scrivi alcuni test per coprire le operazioni di base del componente.
  3. Rifattorizza senza pietà, suddividendo la complicata funzione in molte piccole funzioni semplici, mentre esegui i test insieme per il più spesso possibile.
  4. Spingere il flag "test" il più in alto possibile. Mentre esegui il refactoring, passa le tue origini dati alle piccole semplici funzioni. Non lasciare che il flag "test" infetti qualsiasi funzione tranne quella più in alto.
  5. Riscrivi i test. Mentre esegui il refactoring, riscrivi il maggior numero di test possibile per chiamare le semplici piccole funzioni anziché la grande funzione di livello superiore. Puoi passare le tue derisioni nelle semplici funzioni dei tuoi test.
  6. Sbarazzati del flag 'test' e determina di quale DI hai veramente bisogno. Dato che hai dei test scritti ai livelli inferiori che possono inserire simulazioni attraverso gli aregument, probabilmente non hai più bisogno di deridere molte fonti di dati al livello più alto.

Se, dopo tutto questo, il DI è ancora ingombrante, allora pensa a iniettare un singolo oggetto che contiene riferimenti a tutte le tue origini dati. È sempre più facile iniettare una cosa piuttosto che molte.

Hai ragione sul fatto che può essere ingombrante.

Il fautore della metodologia beffarda sottolineerebbe che il codice è stato scritto in modo improprio. Cioè, non dovresti costruire oggetti dipendenti all'interno di questo metodo. Piuttosto, le API di iniezione dovrebbero avere funzioni che creano gli oggetti appropriati.

Per quanto riguarda deridere 6 oggetti diversi, è vero. Tuttavia, se anche tu testassi l'unità quei sistemi, quegli oggetti dovrebbero già avere un'infrastruttura di derisione che puoi usare.

Infine, usa un framework beffardo che fa parte del lavoro per te.

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 ci sia un costrutto mancante che dovrebbe essere impacchettato in una struttura di livello superiore. L'utilizzo di un contenitore di automocking non fa altro che mettere a dura prova il feedback che ricevi dai tuoi test. Vedi http://www.mockobjects.com/2007/04 /test-smell-bloated-constructor.html per una discussione più lunga.

In questo contesto, di solito trovo delle affermazioni lungo le linee di "questo indica che il tuo oggetto ha troppe dipendenze" oppure " il tuo oggetto ha troppi collaboratori " essere un'affermazione abbastanza speciosa. Naturalmente un controller MVC o un modulo chiamerà molti servizi e oggetti diversi per adempiere ai propri compiti; dopo tutto, si trova sul livello superiore dell'applicazione. È possibile modificare alcune di queste dipendenze insieme in oggetti di livello superiore (ad esempio, un ShippingMethodRepository e un TransitTimeCalculator vengono combinati in un ShippingRateFinder), ma questo va solo così lontano, soprattutto per questi oggetti di livello superiore, orientati alla presentazione. Questo è un oggetto in meno da deridere, ma hai appena offuscato le dipendenze effettive tramite uno strato di indiretto, non effettivamente rimosso.

Un consiglio blasfemo è quello di dire che se stai dipendendo iniettando un oggetto e creando un'interfaccia per esso che è improbabile che cambi mai (Hai davvero intenzione di inserire un nuovo MessageBoxService mentre cambi il tuo codice? Davvero? ), quindi non preoccuparti. Tale dipendenza fa parte del comportamento previsto dell'oggetto e dovresti semplicemente testarli insieme poiché il test di integrazione è il vero valore commerciale.

L'altro consiglio blasfemo è che di solito vedo poca utilità nell'unità di test dei controller MVC o Windows Form. Ogni volta che vedo qualcuno deridere HttpContext e provare per vedere se è stato impostato un cookie, voglio urlare. A chi importa se 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 () non è riuscita dopo Login () nel test di integrazione). In questo modo, eviti di invalidare un milione di test unitari inutili se il formato del cookie di accesso cambia mai.

Salva i test unitari per il modello a oggetti, salva i test di integrazione per il livello di presentazione ed evita, se possibile, oggetti finti. Se prendere in giro una dipendenza particolare è difficile, è tempo di essere pragmatici: semplicemente non fare il test unitario e scrivere un test di integrazione e smettere di perdere tempo.

La semplice risposta è che il codice che stai provando a testare sta facendo troppo . Penso che attenersi al Principio di responsabilità unico potrebbe aiutare.

Il metodo del pulsante Salva deve contenere solo chiamate di primo livello per delegare cose ad altri oggetti . Questi oggetti possono quindi essere astratti attraverso le interfacce. Quindi, quando si verifica il metodo del pulsante Salva, si verifica solo l'interazione con oggetti derisi .

Il prossimo passo è scrivere i test in queste classi di livello inferiore, ma la cosa dovrebbe essere più semplice dato che li provi solo in modo isolato. Se hai bisogno di un codice di installazione di prova complesso, questo è un buon indicatore di una cattiva progettazione (o di un cattivo approccio di prova).

Lettura consigliata:

  1. Clean Code: un manuale di agile software artigianale
  2. Guida di Google per la scrittura di codice testabile

Il costruttore DI non è l'unico modo per fare DI. Dal momento che stai usando C #, se il tuo costruttore non fa un lavoro significativo puoi usare la Proprietà DI. Ciò semplifica notevolmente le cose in termini di costruttori del tuo oggetto a scapito della complessità nella tua funzione. La funzione deve verificare la nullità di qualsiasi proprietà dipendente e generare InvalidOperation se sono null, prima che inizi a funzionare.

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top