2012-02-02 18 views
8

Dato un implementazione Abstract Factory:Unità testare una fabbrica di astratto che accetta parametri

public class FooFactory : IFooFactory { 
    public IFoo Create(object param1, object param2) { 
     return new Foo(param1, param2); 
    } 
} 

Quali esami unità sarebbe scritto per questa classe? Come posso verificare che param1 e param2 siano stati inoltrati alla creazione di Foo? Devo rendere queste proprietà pubbliche di Foo? Non sarebbe un rompersi l'incapsulamento? O dovrei lasciarlo ai test di integrazione?

+0

I test di unità devono dipendere dal requisito funzionale e dalle aspettative sul componente sottoposto a test. Non vedo alcun valore di unità testare quella classe senza il resto del contesto. – ivowiblo

+0

@ivowiblo Penso che tu stia confondendo i test unitari con i test in stile BDD. Se non scrivi un test per questa unità, come fai a sapere se funziona? –

+0

effettua il test dell'unità, ma cosa testerai? qual è l'aspettativa per quell'unità? Ho detto che non vedo il valore di fare un test unitario di quella classe senza sapere altro. Fare test unitari solo per fare test unitari non ha alcun senso. I test (unità, comportamenti, funzionalità complete, qualunque cosa tu voglia) devono essere sempre guidati da aspettative e requisiti. Altrimenti, è solo snobismo (se questa parola esiste). – ivowiblo

risposta

11

Ecco come avrei scritto uno di un paio di test di unità per un tale fabbrica (con xUnit.net):

[Fact] 
public void CreateReturnsInstanceWithCorrectParam1() 
{ 
    var sut = new FooFactory(); 
    var expected = new object(); 
    var actual = sut.Create(expected, new object()); 
    var concrete = Assert.IsAssignableFrom<Foo>(actual); 
    Assert.Equal(expected, concrete.Object1); 
} 

vuol rompere l'incapsulamento? Sì e no ... un po '. L'incapsulamento non riguarda solo l'occultamento dei dati, ma soprattutto l'protecting the invariants of objects.

Supponiamo che Foo espone questa API pubblica:

public class Foo : IFoo 
{ 
    public Foo(object param1, object param2); 

    public void MethodDefinedByInterface(); 

    public object Object1 { get; } 
} 

Mentre la proprietà Object1 leggermente viola la Law of Demeter, non pasticciare con le invarianti della classe perché è di sola lettura.

Inoltre, la proprietà Object1 fa parte della classe Foo concreto - non l'interfaccia IFoo:

public interface IFoo 
{ 
    void MethodDefinedByInterface(); 
} 

Una volta ci si rende conto che in un API loosely coupled, concrete members are implementation details, tali proprietà in calcestruzzo di sola lettura solo hanno un impatto molto basso sull'incapsulamento. Pensare in questo modo:

Il costruttore pubblico Foo è anche una parte delle API della classe Foo concreta, in modo solo controllando l'API pubblica, si apprende che param1 e param2 sono parte della classe. In un certo senso, questo già "rompe l'incapsulamento", quindi rendere ogni parametro disponibile come proprietà di sola lettura sulla classe concreta non cambia molto.

Tali proprietà offrono il vantaggio che ora possiamo testare unitamente la forma strutturale della classe Foo restituita dalla fabbrica.

Questo è molto più facile che dover ripetere una serie di test di unità comportamentali che, dobbiamo presumere, coprono già la classe Foo concreta. È quasi una prova logica:

  1. Dai test della classe di calcestruzzo Foo sappiamo che utilizza/interagisce correttamente con i parametri del costruttore.
  2. Da questi test sappiamo anche che il parametro del costruttore è esposto come proprietà di sola lettura.
  3. Da un test di FooFactory sappiamo che restituisce un'istanza della classe Foo concreta.
  4. Inoltre, dai test del metodo Create sappiamo che i suoi parametri sono passati correttamente al costruttore Foo.
  5. Q.E.D.
+0

Sì, ha senso. Mi piace l'idea di differenziare tra l'API pubblica dell'interfaccia e la classe concreta. – JontyMC

+0

Perché il downvote anonimo? –

0

Si potrebbe rendere FooFactory una classe generica che si aspetta Foo come parametro e specificare che si tratta di un mock. Chiamando factory.Create (...) crea quindi un'istanza del mock, e puoi quindi confermare che il mock ha ricevuto gli argomenti che ti aspettavi.

+2

Caro Gödel n. Questo sta testando un dettaglio di implementazione. – jason

+0

@Jason: Peggio ... Solleva la questione di creare una fabbrica per quella fabbrica per semplificare la generazione dell'oggetto, ma se JontyMC chiede ... – Arafangion

0

Dipende da cosa fa Foo con i 2 oggetti di input. Cerca qualcosa che Foo faccia con loro e che possa essere facilmente interrogato. Ad esempio, se i metodi (inclusi getter o setter) vengono richiamati sugli oggetti, fornire mock o stub che è possibile verificare se le cose previste sono state chiamate. Se c'è qualcosa su IFoo su cui è possibile testare, puoi passare valori noti attraverso i tuoi oggetti mock per testarli dopo la creazione. Non è necessario che gli oggetti stessi siano disponibili al pubblico per verificare che siano stati passati.

È anche possibile verificare che venga restituito il tipo previsto (Foo), a seconda che gli altri comportamenti testati dipendano dalla differenza tra le implementazioni IFoo.

Se ci sono condizioni di errore che possono derivare, mi piacerebbe provare a forzare quelle pure, che cosa succede se si passa in null per uno o entrambi gli oggetti, ecc Foo sarebbe provato separatamente, naturalmente, in modo da può assumere durante un test FooFactory che Foo faccia ciò che dovrebbe.

+0

Proverai il tipo di reso? Cosa diavolo ti sta comprando? Hai impostato tutte queste astrazioni e ora stai per scrivere un test che si interrompe non appena cambi i dettagli di un'implementazione. Yikes. – jason

+0

@Jason Se scriverò un test per più fabbriche che creano ciascuna un'implementazione IFoo, controllerò che ciascuna restituisca il tipo concreto che mi aspetto. Metto in discussione la necessità di testare una fabbrica che fa così poco come chiamare nuova, ma nel regno di ciò che è stato chiesto ... –

+0

No. Non è il comportamento di cui i produttori della fabbrica possono dipendere (il tipo di ritorno è 'IFoo') quindi non dovrebbe essere testato. – jason

0

Le fabbriche di solito non sono testate unitamente, almeno secondo la mia esperienza - non c'è molto valore nell'unità che le prova. Perché è l'implementazione di tale mappatura dell'interfaccia-implementazione, quindi si riferisce l'implementazione concreta. Di conseguenza, non è possibile controllare se riceve i parametri passati.

D'altra parte, di solito i contenitori DI funzionano come fabbriche, quindi se si sta utilizzando uno, non si avrà bisogno di una fabbrica.

+0

Le fabbriche astratte sono obbligatorie quando una dipendenza si basa su parametri conosciuti solo in fase di esecuzione o la durata è inferiore a quella del consumatore: http://stackoverflow.com/questions/1993397/abstract-factory-pattern-on-top- of-ioc – JontyMC

+0

La maggior parte dei framework DI eseguirà il passaggio dei parametri in fase di esecuzione. Possono anche fare una gestione a vita. – Aliostad

2

Bene, presumibilmente questi parametri rendono il IFoo restituito qualcosa di vero su di esso. Verificare che sia vero circa l'istanza restituita.

+0

Non vorrebbe dire che avresti bisogno di sapere degli interni del IFoo in questione? – JontyMC

+0

No. Stai trasmettendo questi parametri per un motivo, per ottenere un comportamento previsto. Qual è questo comportamento? Prova per questo Le specifiche per 'FooFactory.Create' dovrebbero dire qualcosa come" crea un oggetto che implementa 'IFoo' tale che' P (param1, param2) 'dove' P' è un predicato dipendente da 'param1' e' param2'. " Prova che 'P (param1, param2)' è vero per l'oggetto restituito! – jason

+0

Quindi, supponendo che Foo non sia banale e sia già coperto da N test, è necessario scrivere N nuovi test quasi identici per verificare che il comportamento dell'istanza di Foo restituita sia coerente con l'input di Create metodo. Sembra che tu accoppiassi indirettamente i test. Seguiranno problemi di manutenibilità. –

Problemi correlati