Il modo migliore per i metodi di test unitari che chiamano altri metodi all'interno della stessa classe

32

Recentemente stavo discutendo 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 testare questo abbiamo 2 metodi.

Metodo 1: Utilizzare funzioni e 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

    
posta tranceru1 26.02.2013 - 17:02
fonte

4 risposte

28

Modificato dopo l'aggiornamento del poster originale.

Dichiarazione di non responsabilità: non un programmatore C # (principalmente Java o Ruby). La mia risposta sarebbe: non lo testerei affatto, e non penso che dovresti.

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

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

Consulta una discussione correlata qui: link

Dopo il commento , se la funzioneB è pubblica, testerò semplicemente entrambi utilizzando il test dell'unità. Potresti pensare che il test di FunctionA non sia totalmente "unitario" (come chiama FunctionB), ma non sarei troppo preoccupato da questo: se il test FunctionB funziona ma non il test FunctionA, significa chiaramente che il problema non è 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 specie di tecnica di derisione per simulare FunctionB quando testare FunctionA (tipicamente, restituisce un valore corretto noto fisso). Mi manca la conoscenza dell'ecosistema C # per consigliare una libreria di derisione specifica, ma puoi consultare questa domanda .

    
risposta data 26.02.2013 - 17:19
fonte
9

Sottoscrivo la teoria secondo cui se una funzione è importante da testare o è importante da sostituire, è importante non essere un dettaglio di implementazione privato della classe sotto test, ma essere un dettaglio di implementazione pubblica di un < em> different class.

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
     }
}

Allora ho intenzione di 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 indipendentemente e completamente sostituibile.

Come mezzo di organizzazione, il mio collaboratore potrebbe non vivere allo stesso livello di spazio dei nomi. Ad esempio, se A è in FooCorp.BLL, il mio collaboratore potrebbe essere un altro livello profondo, come in FooCorp.BLL.Collaborator (o qualunque nome sia appropriato). Il mio collaboratore potrebbe inoltre essere visibile solo all'interno dell'assieme tramite il modificatore di accesso internal , che esporrò anche ai miei progetti di test dell'unità tramite l'attributo di assemblaggio InternalsVisibleTo . Il takeaway è che puoi mantenere la tua API pulita, per quanto riguarda i chiamanti, mentre produci codice verificabile.

    
risposta data 27.02.2013 - 16:54
fonte
1

Personalmente utilizzo Method1, cioè, realizzo tutti i metodi in Azioni o Func in quanto ciò ha per me un enorme miglioramento della testabilità del codice. Come con qualsiasi soluzione, ci sono pro e contro con questo approccio:

Pro

  1. Consente una struttura di codice semplice in cui l'utilizzo di modelli di codice solo per il test delle unità potrebbe aggiungere ulteriore complessità.
  2. Consente di sigillare le classi e di eliminare i metodi virtuali richiesti da framework di simulazione popolare come Moq. Le classi di sigillatura e l'eliminazione dei metodi virtuali li rendono candidati per l'inlining e altre ottimizzazioni del compilatore. ( link )
  3. Semplifica la testabilità in quanto la sostituzione dell'implementazione Func / Action in un test di unità è semplice come l'assegnazione di un nuovo valore a Func / Action
  4. Permette anche di testare se un Func statico è stato chiamato da un altro metodo poiché i metodi statici non possono essere presi in giro.
  5. Facile da rifattorizzare i metodi esistenti a Func / Actions dalla sintassi per chiamare un metodo sul sito di chiamata rimane lo stesso. (Per i tempi quando non puoi rifattorizzare un metodo in un Func / Action vedi cons)

Contro

  1. Non è possibile utilizzare Func / Actions se la classe può essere derivata dal fatto che Func / Actions non ha percorsi di ereditarietà come Method
  2. Impossibile utilizzare i parametri predefiniti. La creazione di un Func con i 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 dei parametri denominata per chiamare metodi come qualcosa (firstName: "S", lastName: "K")
  4. Il più grande Con è che non si ha accesso al riferimento "this" in Funcs e Actions, quindi tutte le dipendenze della classe devono essere passate esplicitamente come parametri. È buono come lo sai tutte le dipendenze, ma è male se hai molte proprietà da cui dipenderà il tuo Func. Il tuo chilometraggio può variare in base al tuo caso d'uso.

Quindi, per riassumere, usare Funcs e Actions per Unit test è fantastico se sai che le tue classi non saranno mai sovrascritte.

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

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

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

Spero che questo aiuti!

    
risposta data 09.09.2015 - 17:38
fonte
0

Aggiungendo 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 in realtà cambia il comportamento della classe sotto test nel test.

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

Nel problema indicato, vedo che l'unica logica di A è vedere se l'output di FunctionB è pari. Sebbene illustrativo, FunctionB fornisce il valore Random, che è difficile da testare.

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

    
risposta data 27.02.2013 - 09:03
fonte

Leggi altre domande sui tag