Quando utilizzare le interfacce (unit test, IoC?)


17

Ho il sospetto di aver commesso un errore da scolaro qui e sto cercando chiarimenti. Molte classi nella mia soluzione (C #) - oserei dire la maggioranza - ho finito per scrivere un'interfaccia corrispondente per. Ad esempio un'interfaccia "ICalculator" e una classe "Calcolatrice" che la implementa, anche se non è probabile che sostituirò quella calcolatrice con un'implementazione diversa. Inoltre, la maggior parte di queste classi risiede nello stesso progetto delle loro dipendenze: devono solo esserlo internal, ma hanno finito per essere publicun effetto collaterale dell'implementazione delle rispettive interfacce.

Penso che questa pratica di creazione di interfacce per tutto derivasse da alcune menzogne: -

1) Inizialmente pensavo che fosse necessaria un'interfaccia per creare simulazioni di unit test (sto usando Moq), ma da allora ho scoperto che una classe può essere derisa se i suoi membri lo sono virtual, e ha un costruttore senza parametri (correggimi se Mi sbaglio).

2) Inizialmente pensavo fosse necessaria un'interfaccia per registrare una classe con il framework IoC (Castle Windsor), ad es

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

quando in effetti potrei semplicemente registrare il tipo concreto contro se stesso:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) L'uso di interfacce, ad es. Parametri del costruttore per l'iniezione delle dipendenze, provoca un "accoppiamento lento".

Quindi sono impazzito con le interfacce ?! Sono a conoscenza degli scenari in cui "normalmente" useresti un'interfaccia, ad esempio esporre un'API pubblica o per cose come la funzionalità "collegabile". La mia soluzione ha un numero limitato di classi che si adattano a tali casi d'uso, ma mi chiedo se tutte le altre interfacce non sono necessarie e dovrebbero essere rimosse? Riguardo al precedente punto 3), non dovrei violare l '"accoppiamento lento" se dovessi farlo?

Modifica : - Sto solo giocando con Moq, e sembra che i metodi siano pubblici e virtuali, e abbiano un costruttore pubblico senza parametri, per essere in grado di deriderli. Quindi sembra che non posso avere classi interne allora?


Sono abbastanza sicuro che C # non ti imponga di rendere pubblica una classe per implementare un'interfaccia, anche se l'interfaccia è pubblica ...
vaughandroid

1
Non dovresti testare membri privati . Questo può essere visto come un indicatore dell'anti-schema della classe divina. Se senti la necessità di testare le funzionalità private dell'unità, di solito è una buona idea incapsulare quella funzionalità nella sua classe.
Spoike,

@Spoike il progetto in questione è un progetto UI, dove sembrava naturale rendere tutte le classi interne, ma hanno ancora bisogno di test unitari? Ho letto altrove di non aver bisogno di testare metodi interni, ma non ho mai capito i motivi.
Andrew Stephens,

Innanzitutto chiediti qual è l'unità sotto test. È l'intera classe o un metodo? Quell'unità deve essere pubblica per poter essere testata correttamente. Nei progetti UI, di solito separo la logica testabile in controller / modelli-vista / servizi che hanno tutti metodi pubblici. Le viste / i widget effettivi vengono collegati ai controller / modelli di visualizzazione / servizi e testati attraverso regolari test del fumo (ovvero avviare l'app e iniziare a fare clic). È possibile avere scenari che dovrebbero testare la funzionalità sottostante e avere un test unitario sui widget porta a test fragili e dovrebbe essere eseguito con cura.
Spoike,

@Spoike, quindi stai rendendo pubblici i tuoi metodi VM solo per renderli accessibili ai nostri test unitari? Forse è lì che sbaglio, cercando di rendere tutto interno. Penso che le mie mani potrebbero essere legate a Moq, il che sembra richiedere che le classi (non solo i metodi) siano pubbliche per deriderle (vedi il mio commento sulla risposta di Telastyn).
Andrew Stephens,

Risposte:


7

In generale, se crei un'interfaccia che ha un solo implementatore, stai solo scrivendo due volte e perdendo tempo. Le interfacce da sole non forniscono accoppiamento libero se sono strettamente accoppiate a una implementazione ... Detto questo, se vuoi deridere queste cose nei test unitari, di solito è un ottimo segno che avrai inevitabilmente bisogno di più di un implementatore in realtà codice.

Considererei un po 'un odore di codice se quasi tutte le tue classi hanno interfacce. Ciò significa che quasi tutti lavorano l'uno con l'altro in un modo o nell'altro. Sospetterei che le classi stiano facendo troppo (dal momento che non ci sono classi di supporto) o che hai estratto troppo (oh, voglio un'interfaccia di provider per gravità poiché potrebbe cambiare!).


1
Grazie per la risposta. Come accennato, ci sono alcune ragioni sbagliate per cui ho fatto quello che ho fatto, anche se sapevo che la maggior parte delle interfacce non avrebbe mai avuto più di una implementazione. Parte del problema è che sto usando il framework Moq che è ottimo per deridere le interfacce, ma per deridere una classe deve essere pubblico, avere un ctr pubblico senza parametri e i metodi da deridere devono essere "pubblici virtuali").
Andrew Stephens,

La maggior parte delle classi che sto testando le unità sono interne e non voglio renderle pubbliche solo per renderle testabili (anche se potresti argomentare, è per questo che ho scritto tutte queste interfacce). Se mi sbarazzo delle mie interfacce, probabilmente dovrò trovare un nuovo framework beffardo!
Andrew Stephens,

1
@AndrewStephens - non tutto il mondo ha bisogno di essere deriso. Se le classi sono abbastanza solide (o strettamente accoppiate) per non aver bisogno di interfacce, sono abbastanza solide da usare così come sono nei test unitari. Il tempo / la complessità che li contraddistingue non vale i vantaggi del test.
Telastyn,

-1, poiché spesso non sai di quanti implementatori hai bisogno quando scrivi il codice per la prima volta. Se si utilizza un'interfaccia dall'inizio, non è necessario cambiare client durante la scrittura di implementatori aggiuntivi.
Wilbert,

1
@wilbert - certo, una piccola interfaccia è più leggibile della classe, ma non stai scegliendo l'una o l'altra, hai la classe o una classe e un'interfaccia. E potresti leggere troppo nella mia risposta. Non sto dicendo che non creare mai interfacce per una singola implementazione - la maggior parte dovrebbe in effetti, poiché la maggior parte delle interazioni dovrebbe richiedere quella flessibilità. Ma leggi di nuovo la domanda. Creare un'interfaccia per ogni cosa è solo culto del carico. IoC non è un motivo. Le beffe non sono un motivo. I requisiti sono un motivo per implementare tale flessibilità.
Telastyn,

4

1) Anche se le classi concrete sono derisibili, richiede comunque di creare membri virtuale fornire un costruttore senza parametri, che può essere invadente e indesiderato. Tu (o i nuovi membri del team) ti ritroverai presto ad aggiungere sistematicamente virtualcostruttori senza parametri a ogni nuova classe senza pensarci due volte, solo perché "è così che funzionano le cose".

Un'interfaccia è IMO un'idea molto migliore quando è necessario prendere in giro una dipendenza, perché consente di rinviare l'implementazione fino a quando non è realmente necessario e costituisce un bel contratto chiaro della dipendenza.

3) Come è una falsità?

Credo che le interfacce siano fantastiche per definire i protocolli di messaggistica tra oggetti che collaborano. Potrebbero esserci casi in cui la necessità è discutibile, come ad esempio con le entità di dominio . Tuttavia, ovunque tu abbia un partner stabile (cioè una dipendenza iniettata anziché un riferimento transitorio) con cui devi comunicare, dovresti generalmente considerare di utilizzare un'interfaccia.


3

L'idea di IoC è di rendere sostituibili i tipi concreti. Quindi, anche se (in questo momento) hai un solo calcolatore che implementa ICalculator, una seconda implementazione potrebbe dipendere solo dall'interfaccia e non dai dettagli dell'implementazione da Calculator.

Pertanto, la prima versione della registrazione IoC-Container è quella corretta e quella che dovresti usare in futuro.

Se rendi le tue lezioni pubbliche o interne non è realmente correlato, e nemmeno il moq-ing.


2

Sarei davvero più preoccupato del fatto che sembri sentire il bisogno di "deridere" praticamente ogni tipo dell'intero progetto.

I doppi di test sono utili soprattutto per isolare il codice dal mondo esterno (librerie di terze parti, I / O su disco, I / O di rete, ecc.) O da calcoli molto costosi. Essere troppo liberali con la tua struttura di isolamento (ne hai davvero bisogno? Il tuo progetto è così complesso?) Può facilmente condurre a test associati all'implementazione e rende il tuo progetto più rigido. Se scrivi una serie di test che verificano che una classe chiamata metodo X e Y in quell'ordine, e poi passato i parametri a, bec al metodo Z, stai davvero ottenendo valore? A volte si ottengono test come questo quando si testano la registrazione o l'interazione con librerie di terze parti, ma dovrebbe essere l'eccezione, non la regola.

Non appena il tuo framework di isolamento ti dice che devi rendere pubbliche le cose e che devi sempre avere costruttori senza parametri, è tempo di uscire da schivare, perché a questo punto il tuo framework ti costringe a scrivere un codice cattivo (peggio).

Parlando più specificamente delle interfacce, trovo che siano utili in alcuni casi chiave:

  • Quando è necessario inviare chiamate di metodi a tipi diversi, ad esempio quando si hanno più implementazioni (questa è quella ovvia, poiché la definizione di un'interfaccia nella maggior parte delle lingue è solo una raccolta di metodi virtuali)

  • Quando devi essere in grado di isolare il resto del codice da qualche classe. Questo è il modo normale di sottrarre il file system, l'accesso alla rete e il database e così via dalla logica principale dell'applicazione.

  • Quando è necessaria l'inversione del controllo per proteggere i confini dell'applicazione. Questo è uno dei modi più potenti possibili per gestire le dipendenze. Sappiamo tutti che i moduli di alto livello e quelli di basso livello non dovrebbero conoscersi, perché cambieranno per ragioni diverse e NON VUOI avere dipendenze da HttpContext nel tuo modello di dominio o da un framework di rendering Pdf nella tua libreria di moltiplicazione di matrici . Rendere le tue classi di confine implementare interfacce (anche se non vengono mai sostituite) aiuta a sciogliere l'accoppiamento tra i livelli e riduce drasticamente il rischio che le dipendenze filtrino attraverso i livelli da tipi che conoscono troppi altri tipi.


1

Mi affido all'idea che, se una logica è privata, l'implementazione di tale logica non dovrebbe preoccupare nessuno e come tale non dovrebbe essere testata. Se una logica è abbastanza complessa da volerla testare, quella logica ha probabilmente un ruolo distinto e dovrebbe quindi essere pubblica. Ad esempio, se estraggo un po 'di codice in un metodo di supporto privato, trovo che di solito è qualcosa che potrebbe anche stare in una classe pubblica separata (un formattatore, un parser, un mapper, qualunque cosa).
Per quanto riguarda le interfacce, mi lascio guidare dalla necessità di dover stubare o deridere la classe. Roba come i pronti contro termine, di solito voglio essere in grado di stub o deridere. Un mapper in un test di integrazione, (di solito) non tanto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.