Unit test di una classe che utilizza DI senza test su interni


12

Ho una classe che viene refactored in 1 classe principale e 2 classi più piccole. Le classi principali utilizzano il database (come fanno molte delle mie classi) e invia un'e-mail. Quindi la classe principale ha un IPersonRepositorye un IEmailRepositoryiniettato che a sua volta invia alle 2 classi più piccole.

Ora voglio unit test della classe principale e ho imparato a non unittire il funzionamento interno della classe, perché dovremmo essere in grado di cambiare il funzionamento interno senza interrompere i test unitari.

Ma poiché la classe usa IPersonRepositorye e IEmailRepository, DEVO specificare i risultati (fittizi / fittizi) per alcuni metodi per IPersonRepository. La classe principale calcola alcuni dati basati su dati esistenti e li restituisce. Se voglio verificarlo, non vedo come posso scrivere un test senza specificare che IPersonRepository.GetSavingsByCustomerIdrestituisce x. Ma poi il mio test unitario "conosce" i meccanismi interni, perché "sa" quali metodi deridere e quali no.

Come posso testare una classe che ha iniettato dipendenze, senza che il test sia a conoscenza degli interni?

sfondo:

Nella mia esperienza molti test come questo creano simulazioni per i repository e quindi forniscono i dati giusti per le simulazioni o test se durante l'esecuzione è stato chiamato un metodo specifico. In entrambi i casi, il test conosce gli interni.

Ora ho visto una presentazione sulla teoria (che ho sentito prima) che il test non dovrebbe conoscere l'implementazione. In primo luogo perché non esegue il test come funziona, ma anche perché quando si cambia ora l'attuazione tutti gli unit test falliscono perché sanno '' circa l'attuazione. Mentre mi piace l'idea che i test non siano consapevoli dell'implementazione, non so come realizzarlo.


1
Non appena la tua classe principale prevede un IPersonRepositoryoggetto, quell'interfaccia e tutti i metodi che descrive non sono più "interni", quindi non è davvero un problema del test. La tua vera domanda dovrebbe essere "come posso trasformare le classi in unità più piccole senza esporre troppo in pubblico". La risposta è "mantenere snelle quelle interfacce" (attenendosi al principio di seggregation dell'interfaccia, per esempio). Questo è il punto 2 di IMHO nella risposta di @ DavidArno (suppongo che non sia necessario che lo ripeta in un'altra risposta).
Doc Brown,

Risposte:


7

La classe principale calcola alcuni dati basati su dati esistenti e li restituisce. Se voglio verificarlo, non vedo come posso scrivere un test senza specificare che IPersonRepository.GetSavingsByCustomerIdrestituisce x. Ma poi il mio test unitario "conosce" i meccanismi interni, perché "sa" quali metodi deridere e quali no.

Hai ragione sul fatto che si tratta di una violazione del principio "non testare gli interni" ed è un fatto comune che le persone trascurano.

Come posso testare una classe che ha iniettato dipendenze, senza che il test sia a conoscenza degli interni?

Esistono due soluzioni che è possibile adottare per aggirare questa violazione:

1) Fornire una derisione completa di IPersonRepository. Il tuo approccio attualmente descritto è quello di accoppiare la derisione ai meccanismi interni del metodo in esame prendendo in giro solo i metodi che chiamerà. Se fornisci beffe per tutti i metodi di IPersonRepository, rimuovi quell'accoppiamento. I meccanismi interni possono cambiare senza influire sulla derisione, rendendo così il test meno fragile.

Questo approccio ha il vantaggio di mantenere semplice il meccanismo DI, ma può creare molto lavoro se la tua interfaccia definisce molti metodi.

2) Non iniettare IPersonRepository, né iniettare il GetSavingsByCustomerIdmetodo, né iniettare un valore di risparmio. Il problema con l'iniezione di intere implementazioni dell'interfaccia è che si sta iniettando ("dire, non chiedere") un sistema "chiedere, non dire", mescolando i due approcci. Se si adotta un approccio "puro DI", il metodo dovrebbe essere fornito (detto) del metodo esatto da chiamare se desidera un valore di risparmio, piuttosto che ricevere un oggetto (che deve quindi richiedere in modo efficace il metodo da chiamare).

Il vantaggio di questo approccio è che evita la necessità di derisioni (oltre ai metodi di prova che si inietta nel metodo in esame). Lo svantaggio è che può causare la modifica delle firme del metodo in risposta ai requisiti della modifica del metodo.

Entrambi gli approcci hanno i loro pro e contro, quindi scegli quello più adatto alle tue esigenze.


1
Mi piace molto l'idea numero 2. Non so perché non ci abbia mai pensato prima. Sto creando dipendenze da IRepository da un po 'di anni ormai senza mai chiedermi perché ho creato una dipendenza su un intero IRepository (che in molti casi conterrà molte firme di metodi) mentre ha solo una dipendenza da uno o 2 metodi. Quindi, invece di iniettare un IRepository, avrei potuto iniettare o passare meglio la (unica) firma del metodo necessaria.
Michel,

1

Il mio approccio è quello di creare versioni "finte" dei repository che leggono da file semplici che contengono i dati necessari.

Ciò significa che il singolo test non "conosce" l'installazione del mock, anche se ovviamente il progetto di test generale farà riferimento al mock e avrà i file di installazione ecc.

Ciò evita la complessa configurazione di oggetti finti richiesta dai framework di derisione e consente di utilizzare gli oggetti "finti" in istanze reali dell'applicazione per test dell'interfaccia utente e simili.

Poiché il "mock" è completamente implementato, piuttosto che specificamente impostato per lo scenario di test, un cambiamento di implementazione, ad esempio, supponiamo che GetSavingsForCustomer dovrebbe ora eliminare anche il cliente. Non infrangeremo i test (a meno che ovviamente non interrompa davvero i test) devi solo aggiornare la tua singola implementazione fittizia e tutti i tuoi test verranno eseguiti contro di essa senza modificarne la configurazione


2
Questo porta ancora alla situazione in cui creo una simulazione con i dati esattamente per i metodi con cui la classe testata sta lavorando. Quindi ho ancora il problema che quando l'implementazione viene modificata, il test si interromperà.
Michel,

1
modificato per spiegare perché il mio approccio è migliore, sebbene non sia ancora perfetto per lo scenario, penso che intendi
Ewan,

1

I test unitari sono in genere test whitebox (hai accesso al codice reale). Pertanto è bene conoscere gli interni in una certa misura, tuttavia per i principianti è più facile non farlo, perché non si dovrebbe testare il comportamento interno (come "chiamare prima il metodo, poi b e poi di nuovo").

L'iniezione di un mock che fornisce dati è ok, poiché la tua classe (unità) dipende da quei dati esterni (o dal fornitore di dati). Tuttavia, dovresti verificare i risultati (non il modo per raggiungerli)! ad es. si fornisce un'istanza di Persona e si verifica che un messaggio di posta elettronica sia stato inviato all'indirizzo di posta elettronica corretto, ad esempio fornendo anche un mock per l'e-mail, tale mock non fa altro che memorizzare l'indirizzo e-mail del destinatario per un accesso successivo tramite il test -codice. (Penso che Martin Fowler li chiami Stub invece che Mock)

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.