2013-05-23 16 views
6

Ho visto più risposte su "come cancellare le tue classi in modo da poter controllare ciò che accade all'interno del SUT".C# stub. Interfaccia per ogni oggetto testabile?

dicono una cosa:

Creare un'interfaccia e iniettare che si interfacciano con l'iniezione di dipendenza e creare uno stub utilizzando la stessa interfaccia che si poi iniettare nel SUT.

Tuttavia, ciò che ho imparato nei miei precedenti luoghi di lavoro:

Se unit test, si prova tutti classi/funzioni.

Questo significa che per ogni classe che ha una specifica funzione di layout è necessario creare un'interfaccia?

ciò significherebbe la quantità di classi/file sarebbe quasi il doppio di molti.

Come visto nell'esempio qui sotto, è questo 'la strada da percorrere' o mi sto perdendo qualcosa nel mio processo di test di unità?

Come nota: Sto usando VS2012 Express. Ciò significa che non esiste una struttura "Faker". Sto usando il framework di test unitario "standard" VS2012.

Come molto, molto semplice esempio, che mi permette di stub ciascuna interfaccia tramandata ad un SUT.

IFoo.cs

public interface IFoo 
{ 
    string GetName(); 
} 

Foo.cs

public class Foo : IFoo 
{ 
    public string GetName() 
    { 
     return "logic goes here"; 
    } 
} 

IBar.cs:

public interface IBar : IFoo 
{ 
    IFoo GetFoo(); 
} 

Bar.cs:

public class Bar : IBar 
{ 
    public string GetName() 
    { 
     return "logic goes here"; 
    } 

    public IFoo GetFoo() 
    { 
     return null; // some instance of IFoo 
    } 
} 

IBaz.cs:

public interface IBaz 
{ 
    IBar GetBar(); 
} 

Baz.cs:

public class Baz 
{ 
    public IBar GetBar() 
    { 
     return null; // some instance of IBar 
    } 
} 
+3

Il mio consiglio: trovare un buon quadro di simulazione (Moq è una buona scelta) che non richiede di creare tutte queste interfacce. –

+0

Vado a dare un'occhiata a questo framework –

+1

@RobertHarvey: in che modo Moq aiuterà a evitare la creazione di interfacce? L'OP deve comunque lasciare i punti di ingresso per esso (Moq), che si tratti di membri virtuali o interfacce. –

risposta

4

Sì e no. Per sopprimere la dipendenza è necessaria una sorta di astrazione , ma questo è in maggioranza a causa di come i modelli di simulazione work (non tutti, naturalmente).

Considerare un semplice esempio. Si prova la classe A che accetta le dipendenze dalle classi B e C. Per i test unitari di A per il funzionamento, è necessario simulare B e C - è necessario IB e IC (o classi di base/w membri virtuali). Hai bisogno di IA? No, almeno non per questo test. E a meno che lo A non diventi dipendente da qualche altra classe, l'astrazione all'interfaccia/non è richiesta la classe base.

L'astrazione è ottima in quanto consente di creare codice abbinato. Dovresti astrarre le tue dipendenze. Tuttavia, in pratica alcune classi non devono essere astratte in quanto servono i ruoli di livello superiore/fine della gerarchia/radice e non vengono utilizzate altrove.

+0

Quindi si dovrebbe utilizzare le funzioni virtuali anziché utilizzare un'interfaccia (dove l'interfaccia non è necessaria). Questo comporta un comportamento extra indesiderato? Non mi dispiacerebbe che le classi dei bambini ereditassero dalle mie classi e scavalchiassero ogni metodo. –

+0

@DaanTimmer: Dipende dalla base per classe. Tuttavia, nella maggior parte dei casi si desidera utilizzare le interfacce. Le classi base e l'ereditarietà sono di solito più difficili da comprendere e di conseguenza più difficili da gestire rispetto alla composizione ([vedi qui] (http://en.wikipedia.org/wiki/Composition_over_inheritance)). Naturalmente, quando la classe base è giustificata, è quello che dovresti fare. Un altro punto: se il tuo unico argomento contro le interfacce è "raddoppieranno il numero dei tipi", allora non è affatto un argomento. Non dovresti abbandonare la progettazione corretta per ottenere guadagni artificiali in numeri/statistiche. –

+0

@DaanTimmer: Inoltre, aprire la tua classe in questo modo (rendendo i suoi membri virtuali) raramente è una buona idea, specialmente quando non c'è una ragione giustificata (e "meno tipi" non è una sola). –

3

Forse dal punto di vista purista che è il modo giusto per vai, ma la cosa veramente importante è assicurarsi che le dipendenze esterne (es database, accesso alla rete, ecc.), tutto ciò che è dispendioso dal punto di vista computazionale/dispendioso in termini di tempo e tutto ciò che non è completamente deterministico viene sottratto e facilmente sostituibile nei test delle unità.

6

A mio parere, non è consigliabile creare interfacce solo a scopo di test delle unità. Se inizi ad aggiungere astrazioni di codice per soddisfare gli strumenti, allora non ti stanno aiutando ad essere più produttivo. Il codice che scrivi dovrebbe idealmente servire a uno specifico scopo/bisogno di business, direttamente o indirettamente, rendendo il codice base più facile da mantenere o evolvere.

Le interfacce a volte lo fanno, ma certamente non sempre. Trovo che fornire interfacce per componenti sia di solito una buona cosa, ma cerca di evitare l'uso di interfacce per le classi interne (ovvero, il codice viene utilizzato solo all'interno del progetto dato, indipendentemente dal fatto che i tipi siano dichiarati pubblici o meno). Questo perché un componente (come in, un insieme di classi che lavorano insieme per risolvere alcuni problemi specifici) rappresenta un concetto più ampio (come un logger o uno schedulatore), che è qualcosa che potrei voler sostituire o stub fuori durante i test .

La soluzione (la punta di diamante di Robert per essere la prima nei commenti) consiste nell'utilizzare un framework di simulazione per generare un tipo di sostituzione compatibile in fase di esecuzione. I framework di simulazione consentono quindi di verificare che la classe sottoposta a test abbia interagito correttamente con il manichino sostitutivo. Moq è come detto una scelta elegante. Rhino.Mocks e NMock sono altri due framework popolari. Typemock Isolator si aggancia al profiler ed è tra le opzioni più potenti (consente di sostituire anche membri privati ​​non virtuali), ma è uno strumento commerciale.

Non va bene stabilire le regole per quanto si dovrebbe testare l'unità. Dipende da cosa stai sviluppando e quali sono i tuoi obiettivi - se la correttezza supera sempre il time-to-market e il costo non è un fattore, allora l'unità che testa tutto è eccezionale. La maggior parte delle persone non è così fortunata e dovrà scendere a compromessi per raggiungere un livello ragionevole di copertura del test. Quanto dovresti testare può dipendere anche dal livello generale delle competenze del team, dalla durata prevista e dal riutilizzo del codice scritto, ecc.

+0

Per fortuna non ho problemi con i gestori e il time-to-market. Sono solo uno sviluppatore unico che fa questo nel mio tempo libero per imparare qualcosa di extra che non posso imparare nel mio lavoro (in questo momento). (Sono un Embedded S.E. che usa C in questo momento, a volte assembly, a volte C#, a volte C++) quindi sì. Ora sto solo cercando i miei modi per testare le unità C#. Precedentemente ha eseguito un MOLTO test delle unità in Python (relativo al lavoro) –

0

Dal punto di vista del test, non è necessario creare un'interfaccia per ogni classe nel tuo codice. Si crea un'interfaccia per nascondere l'esecuzione concreta di dipendenze esterne dietro un livello di astrazione. Quindi, invece di avere una classe che richiede una connessione HTTP diretta combinata con la tua logica, devi isolare il codice di connessione in una classe, avere implementare un'interfaccia che sia un membro della tua classe e iniettare una falsa posizione in quell'interfaccia . In questo modo, puoi testare la tua logica in modo indipendente, privo di dipendenza, e l'unico codice "non testato" è codice di connessione HTTP boilerplate che può essere testato con altri mezzi.

0

Avrei scelto il percorso del metodo virtuale.Creare interfacce per ogni classe che devi testare diventa davvero pesante, soprattutto quando hai bisogno di strumenti come Resharper per il "passaggio all'implementazione" ogni volta che desideri vedere la definizione di un metodo. E c'è il sovraccarico di gestire e modificare entrambi i file ogni volta che viene modificata una firma del metodo o viene aggiunta una nuova proprietà o un nuovo metodo.