Der beste Weg, um Unit-Test-Methoden zu erstellen, die andere Methoden innerhalb derselben Klasse aufrufen


35

Ich habe kürzlich mit einigen Freunden diskutiert, welche der folgenden 2 Methoden am besten geeignet ist, um Ergebnisse oder Aufrufe von Methoden innerhalb derselben Klasse von Methoden innerhalb derselben Klasse zurückzugeben.

Dies ist ein sehr vereinfachtes Beispiel. In Wirklichkeit sind die Funktionen viel komplexer.

Beispiel:

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

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

Um dies zu testen, haben wir 2 Methoden.

Methode 1: Verwenden Sie Funktionen und Aktionen, um die Funktionalität der Methoden zu ersetzen. Beispiel:

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

Methode 2: machen Sie Mitglieder virtuell, leiten Sie Klasse ab, und verwenden Sie in abgeleiteter Klasse Funktionen und Aktionen, um Funktionalität zu ersetzen Beispiel:

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

Ich möchte wissen, was besser ist und auch WARUM?

Update: HINWEIS: FunctionB kann auch öffentlich sein


Ihr Beispiel ist einfach, aber nicht genau richtig. FunctionAGibt einen Bool zurück, setzt aber nur eine lokale Variable xund gibt nichts zurück.
Eric P.

1
In diesem speziellen Beispiel kann sich FunctionB public staticnur in einer anderen Klasse befinden.

Für die Codeüberprüfung wird erwartet, dass Sie den tatsächlichen Code und keine vereinfachte Version veröffentlichen. Siehe die FAQ. Derzeit stellen Sie eine bestimmte Frage, ohne nach einer Codeüberprüfung zu suchen.
Winston Ewert

1
FunctionBist defekt durch Design. new Random().Next()ist fast immer falsch. Sie sollten die Instanz von injizieren Random. ( Randomist auch eine schlecht gestaltete Klasse, die ein paar zusätzliche Probleme verursachen kann)
CodesInChaos

auf einer allgemeineren Note DI über Delegierten ist absolut in Ordnung imho
jk.

Antworten:


32

Nach Original-Poster-Update bearbeitet.

Haftungsausschluss: Kein C # -Programmierer (hauptsächlich Java oder Ruby). Meine Antwort wäre: Ich würde es überhaupt nicht testen, und ich denke nicht, dass Sie sollten.

Die längere Version lautet: Private / Protected-Methoden sind nicht Teil der API. Sie stellen im Grunde genommen Implementierungsoptionen dar, die Sie überprüfen, aktualisieren oder vollständig verwerfen können, ohne dass dies Auswirkungen auf die Außenwelt hat.

Ich nehme an, Sie haben einen Test für FunctionA (), den Teil der Klasse, der von der Außenwelt aus sichtbar ist. Es sollte der einzige sein, der einen Vertrag zu implementieren hat (und der getestet werden könnte). Ihre private / geschützte Methode hat keinen Vertrag zum Erfüllen und / oder Testen.

Siehe eine verwandte Diskussion dort: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Wenn FunctionB öffentlich ist, teste ich nach dem Kommentar einfach beide mit Unit-Test. Sie mögen denken, dass der Test von FunctionA nicht ganz "Einheit" ist (wie er FunctionB nennt), aber ich würde mir keine Sorgen machen: Wenn der FunctionB-Test funktioniert, aber der FunctionA-Test nicht, bedeutet das, dass das Problem nicht in der liegt Subdomain von FunctionB, was für mich als Diskriminator gut genug ist.

Wenn Sie wirklich in der Lage sein möchten, die beiden Tests vollständig zu trennen, würde ich beim Testen von FunctionA eine Art Verspottungstechnik verwenden, um FunctionB zu verspotten (normalerweise wird ein fester bekannter korrekter Wert zurückgegeben). Mir fehlt das Wissen über das C # -Ökosystem, um eine bestimmte Spottbibliothek zu beraten, aber Sie können sich diese Frage ansehen .


2
Stimmen Sie der Antwort von @Martin voll und ganz zu. Wenn Sie Komponententests für Klassen schreiben, sollten Sie keine Methoden testen . Was Sie testen, ist ein Klassenverhalten , dass der Vertrag (die Erklärung, welche Klasse tun soll) erfüllt ist. Daher sollten Ihre

Hallo, danke für die Antwort, aber meine Frage wurde nicht beantwortet. Es ist mir egal, ob FunctionB privat / geschützt war. Es kann auch öffentlich sein und trotzdem von FunctionA aufgerufen werden.

Die gebräuchlichste Methode zur Behebung dieses Problems ohne Neugestaltung der Basisklasse besteht darin, eine Unterklasse zu erstellen MyClassund die Methode mit der Funktionalität zu überschreiben, die Sie stubben möchten. Es kann auch eine gute Idee sein, Ihre Frage zu aktualisieren, FunctionBdamit sie öffentlich ist.
Eric P.

1
protectedMethoden sind Teil der öffentlichen Oberfläche einer Klasse, es sei denn, Sie stellen sicher, dass Ihre Klasse nicht in verschiedenen Assemblys implementiert werden kann.
CodesInChaos

2
Die Tatsache, dass FunctionA FunctionB aufruft, ist aus Sicht der Einheitentests ein irrelevantes Detail. Wenn die Tests für FunctionA korrekt geschrieben sind, handelt es sich um ein Implementierungsdetail, das später überarbeitet werden kann, ohne die Tests zu unterbrechen (sofern das Gesamtverhalten von FunctionA unverändert bleibt). Das eigentliche Problem ist, dass beim Abrufen einer Zufallszahl durch FunctionB ein injiziertes Objekt verwendet werden muss, damit Sie während des Tests einen Schein verwenden können, um sicherzustellen, dass eine bekannte Zahl zurückgegeben wird. Auf diese Weise können Sie bekannte Ein- / Ausgänge testen.
Dan Lyons

11

Ich stimme der Theorie zu, dass eine zu testende oder zu ersetzende Funktion wichtig genug ist, um kein privates Implementierungsdetail der zu testenden Klasse, sondern ein öffentliches Implementierungsdetail einer anderen Klasse zu sein.

Also, wenn ich in einem Szenario bin, in dem ich habe

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

Dann werde ich umgestalten.

class A 
{
     ICollaborator collaborator;

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

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

Jetzt habe ich ein Szenario, in dem D () unabhängig testbar und vollständig ersetzbar ist.

Aus organisatorischen Gründen befindet sich mein Mitarbeiter möglicherweise nicht auf derselben Namespace-Ebene. ABefindet sich mein Mitarbeiter beispielsweise in FooCorp.BLL, ist er möglicherweise eine weitere Ebene tiefer als in FooCorp.BLL.Collaborators (oder wie auch immer der Name lautet). Mein Mitarbeiter ist möglicherweise nur über den internalZugriffsmodifikator in der Assembly sichtbar , den ich dann auch über das InternalsVisibleToAssemblyattribut für meine Unit-Test-Projekte verfügbar mache. Der Vorteil ist, dass Sie Ihre API für Anrufer weiterhin sauber halten und gleichzeitig überprüfbaren Code erstellen können.


Ja, wenn ICollaborator mehrere Methoden benötigt. Wenn Sie ein Objekt haben, dessen einzige Aufgabe darin besteht, eine einzelne Methode zu verpacken, wird es lieber durch einen Stellvertreter ersetzt.
jk.

Sie müssten sich entscheiden, ob ein benannter Delegat Sinn macht oder ob eine Schnittstelle Sinn macht, und ich werde mich nicht für Sie entscheiden. Ich persönlich bin einzelnen (öffentlichen) Methodenklassen nicht abgeneigt. Je kleiner desto besser, da sie immer verständlicher werden.
Anthony Pegram

0

Ergänzend zu dem, was Martin betont,

Wenn Ihre Methode privat / geschützt ist, testen Sie sie nicht. Es ist innerhalb der Klasse und sollte nicht außerhalb der Klasse zugegriffen werden.

In beiden von Ihnen erwähnten Ansätzen habe ich folgende Bedenken:

Methode 1 - Dies ändert tatsächlich das Verhalten der zu testenden Klasse im Test.

Methode 2 - Hiermit wird der Produktionscode nicht getestet, sondern eine andere Implementierung.

In dem angegebenen Problem sehe ich, dass die einzige Logik von A darin besteht, zu sehen, ob die Ausgabe von FunctionB gerade ist. Obwohl es nur zur Veranschaulichung dient, gibt FunctionB einen Zufallswert an, der schwer zu testen ist.

Ich erwarte ein realistisches Szenario, in dem wir MyClass so einrichten können, dass wir wissen, welche FunctionB zurückgeben würde. Wenn unser erwartetes Ergebnis bekannt ist, können wir FunctionA aufrufen und das tatsächliche Ergebnis bestätigen.


3
protectedist fast das gleiche wie public. Nur privateund internalsind Implementierungsdetails.
CodesInChaos

@codeinchaos - ich bin neugierig hier. Für einen Test sind geschützte Methoden "privat", es sei denn, Sie ändern Assemblyattribute. Nur abgeleitete Typen haben Zugriff auf geschützte Mitglieder. Mit Ausnahme von virtual verstehe ich nicht, warum protected wie public aus einem Test heraus behandelt werden sollte. Könnten Sie bitte näher darauf eingehen?
Srikanth Venugopalan

Da sich diese abgeleiteten Klassen in verschiedenen Assemblys befinden können, sind sie Code von Drittanbietern ausgesetzt und somit Teil der öffentlichen Oberfläche Ihrer Klasse. Zum Testen können Sie sie internal protectederstellen, einen privaten Reflection-Helfer verwenden oder eine abgeleitete Klasse in Ihrem Testprojekt erstellen.
CodesInChaos

@CodesInChaos war sich einig, dass abgeleitete Klassen in verschiedenen Assemblys vorhanden sein können, der Gültigkeitsbereich jedoch weiterhin auf die Basis- und abgeleiteten Typen beschränkt ist. Ich bin etwas nervös, wenn ich den Zugriffsmodifikator ändere, um ihn testbar zu machen. Ich habe es getan, aber es scheint mir ein Gegenmuster zu sein.
Srikanth Venugopalan

0

Ich persönlich verwende Method1, dh, ich verwandle alle Methoden in Actions oder Funcs, da dies die Code-Testbarkeit für mich enorm verbessert hat. Wie bei jeder Lösung gibt es Vor- und Nachteile bei diesem Ansatz:

Vorteile

  1. Ermöglicht eine einfache Codestruktur, bei der die Verwendung von Codemustern nur für Komponententests die Komplexität erhöhen kann.
  2. Ermöglicht das Versiegeln von Klassen und das Eliminieren von virtuellen Methoden, die von gängigen Spott-Frameworks wie Moq benötigt werden. Durch das Versiegeln von Klassen und das Eliminieren virtueller Methoden sind sie Kandidaten für Inlining- und andere Compileroptimierungen. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Vereinfacht die Testbarkeit, da das Ersetzen der Func / Action-Implementierung in einem Unit-Test so einfach ist, als würde der Func / Action lediglich ein neuer Wert zugewiesen
  4. Ermöglicht auch das Testen, ob eine statische Func von einer anderen Methode aufgerufen wurde, da statische Methoden nicht verspottet werden können.
  5. Bestehende Methoden lassen sich einfach in Funcs / Actions umgestalten, da die Syntax zum Aufrufen einer Methode auf der aufgerufenen Site unverändert bleibt. (In Zeiten, in denen Sie eine Methode nicht in eine Funktion / Aktion umgestalten können, siehe Nachteile.)

Nachteile

  1. Funktionen / Aktionen können nicht verwendet werden, wenn Ihre Klasse abgeleitet werden kann, da Funktionen / Aktionen keine Vererbungspfade wie Methoden haben
  2. Standardparameter können nicht verwendet werden. Das Erstellen einer Funktion mit Standardparametern erfordert das Erstellen eines neuen Delegaten, wodurch der Code je nach Anwendungsfall möglicherweise verwirrend wird
  3. Die benannte Parametersyntax kann nicht zum Aufrufen von Methoden wie "firstName:" S ", lastName:" K "verwendet werden.
  4. Der größte Nachteil ist, dass Sie keinen Zugriff auf die 'this'-Referenz in Funcs and Actions haben. Daher müssen alle Abhängigkeiten von der Klasse explizit als Parameter übergeben werden. Es ist gut, wie Sie alle Abhängigkeiten kennen, aber schlecht, wenn Sie viele Eigenschaften haben, von denen Ihr Func abhängt. Ihr Kilometerstand hängt von Ihrem Anwendungsfall ab.

Zusammenfassend lässt sich sagen, dass die Verwendung von Funktionen und Aktionen für den Komponententest großartig ist, wenn Sie wissen, dass Ihre Klassen niemals außer Kraft gesetzt werden.

Außerdem erstelle ich normalerweise keine Eigenschaften für Funcs, sondern binde sie direkt als solche ein

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

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

Hoffe das hilft!


-1

Mit Mock ist es möglich. Nuget: https://www.nuget.org/packages/moq/

Und glauben Sie mir, es ist ziemlich einfach und macht Sinn.

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

Mock braucht eine virtuelle Methode zum Überschreiben.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.