Il modo migliore per testare metodi unitari che chiamano altri metodi all'interno della stessa classe


35

Di recente ho discusso con alcuni amici su quale dei seguenti 2 metodi è meglio stub restituire risultati o chiamate a metodi all'interno della stessa classe da metodi all'interno della stessa classe.

Questo è un esempio molto semplificato. In realtà le funzioni sono molto più complesse.

Esempio:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Quindi per testarlo abbiamo 2 metodi.

Metodo 1: utilizzare le funzioni e le azioni per sostituire la funzionalità dei metodi. Esempio:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Metodo 2: rendere i membri virtuali, derivare la classe e nella classe derivata utilizzare Funzioni e azioni per sostituire la funzionalità Esempio:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Voglio sapere qual è il migliore e anche PERCHÉ?

Aggiornamento: NOTA: FunctionB può anche essere pubblico


Il tuo esempio è semplice, ma non esattamente corretto. FunctionArestituisce un valore booleano ma imposta solo una variabile locale xe non restituisce nulla.
Eric P.

1
In questo esempio particolare, FunctionB può public staticappartenere a una classe diversa.

Per Code Review, ci si aspetta che pubblichi un codice effettivo non una versione semplificata di esso. Vedi le FAQ. Allo stato attuale, stai ponendo una domanda specifica non cercando una revisione del codice.
Winston Ewert,

1
FunctionBè rotto dal design. new Random().Next()è quasi sempre sbagliato. Dovresti iniettare l'istanza di Random. ( Randomè anche una classe mal progettata, che può causare alcuni problemi aggiuntivi)
CodesInChaos

su una di note più generale tramite delegati è assolutamente bene imho
JK.

Risposte:


32

Modificato in seguito all'aggiornamento del poster originale.

Disclaimer: non un programmatore C # (principalmente Java o Ruby). La mia risposta sarebbe: non la testerei affatto e non penso che dovresti.

La versione più lunga è: i metodi privati ​​/ protetti non fanno parte dell'API, sono fondamentalmente scelte di implementazione, che puoi decidere di rivedere, aggiornare o eliminare completamente senza alcun impatto all'esterno.

Suppongo che tu abbia un test su FunctionA (), che è la parte della classe che è visibile dal mondo esterno. Dovrebbe essere l'unico che ha un contratto da implementare (e che potrebbe essere testato). Il tuo metodo privato / protetto non ha alcun contratto da adempiere e / o testare.

Vedi una discussione correlata qui: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Dopo il commento , se FunctionB è pubblico, testerò semplicemente entrambi usando unit test. Potresti pensare che il test di FunctionA non sia totalmente "unitario" (come chiama FunctionB), ma non sarei troppo preoccupato per questo: se il test FunctionB funziona ma non il test FunctionA, significa chiaramente che il problema non si trova nel sottodominio di FunctionB, che è abbastanza buono per me come discriminatore.

Se vuoi davvero essere in grado di separare completamente i due test, userei una sorta di tecnica di derisione per deridere FunctionB quando collaudo FunctionA (in genere, restituisce un valore corretto noto fisso). Mi manca la conoscenza dell'ecosistema C # per consigliare una libreria di derisione specifica, ma puoi dare un'occhiata a questa domanda .


2
Completamente d'accordo con la risposta di @Martin. Quando si scrivono unit test per la classe non è necessario testare i metodi . Quello che stai testando è un comportamento di classe , che il contratto (la dichiarazione che classe dovrebbe fare) è soddisfatto. Quindi, i tuoi test unitari dovrebbero coprire tutti i requisiti esposti per questa classe (usando metodi / proprietà pubblici), compresi casi eccezionali

Ciao, grazie per la risposta, ma non ha risposto alla mia domanda. Non importa se FunctionB era privato / protetto. Può anche essere pubblico e può ancora essere chiamato da FunctionA.

Il modo più comune per gestire questo problema, senza riprogettare la classe base, è sottoclassare MyClasse sovrascrivere il metodo con la funzionalità che si desidera stub. Potrebbe anche essere una buona idea aggiornare la tua domanda per includere che FunctionBpotrebbe essere pubblico.
Eric P.

1
protectedi metodi fanno parte della superficie pubblica di una classe, a meno che non ci si assicuri che non ci possano essere implementazioni della propria classe in assiemi diversi.
CodesInChaos,

2
Il fatto che FunctionA chiama FunctionB è un dettaglio irrilevante dal punto di vista del test unitario. Se i test per FunctionA sono scritti correttamente, si tratta di un dettaglio di implementazione che potrebbe essere rifattorizzato successivamente senza interrompere i test (a condizione che il comportamento generale di FunctionA rimanga invariato). Il vero problema è che il recupero da parte di FunctionB di un numero casuale deve essere fatto con un oggetto iniettato, in modo da poter usare un finto durante il test per assicurarsi che venga restituito un numero noto. Ciò consente di testare input / output noti.
Dan Lyons,

11

Sottoscrivo la teoria secondo cui se una funzione è importante da testare o è importante sostituirla, è abbastanza importante non essere un dettaglio dell'implementazione privata della classe sottoposta a test, ma essere un dettaglio dell'implementazione pubblica di una classe diversa .

Quindi, se sono in uno scenario in cui ho

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Poi vado a refactoring.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Ora ho uno scenario in cui D () è testabile in modo indipendente e completamente sostituibile.

Come mezzo di organizzazione, il mio collaboratore potrebbe non vivere allo stesso livello di spazio dei nomi. Ad esempio, se si Atrova in FooCorp.BLL, il mio collaboratore potrebbe trovarsi in un altro livello profondo, come in FooCorp.BLL.Collaborators (o con qualsiasi nome appropriato). Il mio collaboratore potrebbe inoltre essere visibile solo all'interno dell'assembly tramite il internalmodificatore di accesso, che poi esponerei anche ai miei progetti di test di unità tramite l' InternalsVisibleToattributo assembly. L'aspetto da asporto è che puoi comunque mantenere pulita la tua API, per quanto riguarda i chiamanti, producendo codice verificabile.


Sì, se ICollaborator necessita di diversi metodi. Se hai un oggetto il cui unico compito è avvolgere un singolo metodo, preferirei vederlo sostituito con un delegato.
jk.

Dovresti decidere se un delegato nominato ha un senso o se un'interfaccia ha un senso e io non deciderò per te. Personalmente, non sono contrario alle singole classi di metodo (pubbliche). Più piccolo è il migliore, in quanto diventano sempre più facili da capire.
Anthony Pegram,

0

Aggiungendo a ciò che Martin sottolinea,

Se il tuo metodo è privato / protetto, non testarlo. È interno alla classe e non dovrebbe essere accessibile al di fuori della classe.

In entrambi gli approcci che hai citato, ho queste preoccupazioni:

Metodo 1: questo modifica effettivamente la classe sotto il comportamento del test nel test.

Metodo 2: in realtà non verifica il codice di produzione, ma verifica un'altra implementazione.

Nel problema dichiarato, vedo che l'unica logica di A è vedere se l'uscita di FunctionB è pari. Sebbene illustrativo, FunctionB fornisce un valore Casuale, che è difficile da testare.

Mi aspetto uno scenario realistico in cui possiamo configurare MyClass in modo tale da sapere cosa restituirebbe FunctionB. Quindi il nostro risultato atteso è noto, possiamo chiamare FunctionA e affermare il risultato effettivo.


3
protectedè quasi uguale a public. Solo privatee internalsono dettagli di implementazione.
CodesInChaos,

@codeinchaos - Sono curioso qui. Per un test, i metodi protetti sono "privati" a meno che non si modifichino gli attributi di assembly. Solo i tipi derivati ​​hanno accesso ai membri protetti. Ad eccezione del virtuale, non vedo perché un protetto debba essere trattato in modo simile al pubblico da un test. potresti elaborare per favore?
Srikanth Venugopalan,

Poiché tali classi derivate possono trovarsi in assiemi diversi, sono esposte a codice di terze parti e quindi parte della superficie pubblica della classe. Per testarli, puoi crearli internal protected, utilizzare un helper di riflessione privato o creare una classe derivata nel tuo progetto di test.
CodesInChaos,

@CodesInChaos, ha convenuto che la classe derivata può trovarsi in assiemi diversi, ma l'ambito è ancora limitato ai tipi base e derivati. Modificare il modificatore di accesso solo per renderlo testabile è qualcosa di cui sono un po 'nervoso. L'ho fatto, ma sembra essere un antipasto per me.
Srikanth Venugopalan,

0

Personalmente uso Method1, ovvero trasformando tutti i metodi in Actions o Funcs in quanto ciò ha migliorato enormemente la testabilità del codice per me. Come con qualsiasi soluzione, ci sono pro e contro con questo approccio:

Professionisti

  1. Consente una semplice struttura del codice in cui l'utilizzo di Code Patterns solo per Unit Testing potrebbe aggiungere ulteriore complessità.
  2. Consente alle classi di essere sigillate e per l'eliminazione dei metodi virtuali richiesti dai framework di derisione popolari come Moq. Sigillare le classi ed eliminare i metodi virtuali li rende candidati per l'ottimizzazione e altre ottimizzazioni del compilatore. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Semplifica la testabilità in quanto sostituire l'implementazione Func / Action in un Unit test è semplice come assegnare un nuovo valore a Func / Action
  4. Consente anche di verificare se un Func statico è stato chiamato da un altro metodo poiché i metodi statici non possono essere derisi.
  5. Facilità di refactoring dei metodi esistenti in Func / Actions poiché la sintassi per chiamare un metodo sul sito di chiamata rimane la stessa. (Per i momenti in cui non è possibile trasformare un metodo in un Func / Action, vedere i contro)

Contro

  1. Funcs / Actions non possono essere usati se la tua classe può essere derivata poiché Funcs / Actions non hanno percorsi di ereditarietà come Metodi
  2. Impossibile utilizzare i parametri predefiniti. La creazione di un Func con parametri predefiniti comporta la creazione di un nuovo delegato che potrebbe rendere il codice confuso a seconda del caso d'uso
  3. Impossibile utilizzare la sintassi del parametro denominato per chiamare metodi come qualcosa (firstName: "S", lastName: "K")
  4. Il più grande svantaggio è che non si ha accesso al riferimento 'this' in Funcs e Actions, e quindi tutte le dipendenze sulla classe devono essere esplicitamente passate come parametri. È buono come sai tutte le dipendenze, ma male se hai molte proprietà da cui dipenderà il tuo Func. Il chilometraggio varia in base al caso d'uso.

Quindi, per riassumere, usare Funcs e Actions per Unit test è ottimo se sai che le tue classi non verranno mai ignorate.

Inoltre, di solito non creo proprietà per Func ma le inserisco direttamente come tali

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

Spero che sia di aiuto!


-1

Utilizzando Mock è possibile. Nuget: https://www.nuget.org/packages/moq/

E credimi è abbastanza semplice e ha senso.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Il mock ha bisogno del metodo virtuale per sovrascrivere.

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.