Wie teste ich eine Funktion, die auf das Strategiemuster umgestaltet wurde?

10

Wenn ich eine Funktion in meinem Code habe, die wie folgt aussieht:

class Employee{

    public string calculateTax(string name, int salary)
    {
        switch (name)
        {
            case "Chris":
                doSomething($salary);
            case "David":
                doSomethingDifferent($salary);
            case "Scott":
               doOtherThing($salary);               
       }
}

Normalerweise würde ich dies umgestalten, um Ploymorphism unter Verwendung einer Fabrikklasse und eines Strategiemusters zu verwenden:

public string calculateTax(string name)
{
    InameHandler nameHandler = NameHandlerFactory::getHandler(name);
    nameHandler->calculateTax($salary);
}

Wenn ich jetzt TDD verwenden würde, hätte ich calculateTax()vor dem Refactoring einige Tests, die mit dem Original funktionieren .

Ex:

calculateTax_givenChrisSalaryBelowThreshold_Expect111(){}    
calculateTax_givenChrisSalaryAboveThreshold_Expect111(){}

calculateTax_givenDavidSalaryBelowThreshold_Expect222(){}   
calculateTax_givenDavidSalaryAboveThreshold_Expect222(){} 

calculateTax_givenScottSalaryBelowThreshold_Expect333(){}
calculateTax_givenScottSalaryAboveThreshold_Expect333(){}

Nach dem Refactoring habe ich eine Factory-Klasse NameHandlerFactoryund mindestens 3 Implementierungen von InameHandler.

Wie soll ich vorgehen, um meine Tests zu überarbeiten? Soll ich den Unit - Test für löschen claculateTax()aus EmployeeTestsund eine Test - Klasse für jede Implementierung schaffen InameHandler?

Soll ich auch die Factory-Klasse testen?

Songo
quelle

Antworten:

6

Die alten Tests sind in Ordnung, um zu überprüfen, ob sie calculateTaxnoch ordnungsgemäß funktionieren. Sie benötigen hierfür jedoch nicht viele Testfälle, nur 3 (oder weitere, wenn Sie auch die Fehlerbehandlung mit unerwarteten Werten von testen möchten name).

Jeder der Einzelfälle (derzeit in doSomethinget al. Implementiert ) muss auch seine eigenen Tests haben, die die inneren Details und Sonderfälle in Bezug auf jede Implementierung testen. Im neuen Setup könnten / sollten diese Tests in direkte Tests für die jeweilige Strategieklasse umgewandelt werden.

Ich ziehe es vor, alte Komponententests nur dann zu entfernen, wenn der von ihnen ausgeübte Code und die darin implementierte Funktionalität vollständig nicht mehr existieren. Andernfalls ist das in diesen Tests kodierte Wissen immer noch relevant, nur die Tests müssen selbst überarbeitet werden.

Aktualisieren

Zwischen den Tests von calculateTax(nennen wir sie High-Level-Tests ) und den Tests für die einzelnen Berechnungsstrategien ( Low-Level-Tests ) kann es zu Überschneidungen kommen - dies hängt von Ihrer Implementierung ab.

Ich vermute, dass die ursprüngliche Implementierung Ihrer Tests das Ergebnis der spezifischen Steuerberechnung bestätigt und implizit überprüft, ob die spezifische Berechnungsstrategie verwendet wurde, um sie zu erstellen. Wenn Sie dieses Schema beibehalten, werden Sie tatsächlich dupliziert. Wie @Kristof angedeutet hat, können Sie die Tests auf hoher Ebene auch mithilfe von Mocks implementieren, um nur zu überprüfen, ob die richtige Art von (Schein-) Strategie von ausgewählt und aufgerufen wurde calculateTax. In diesem Fall gibt es keine Überschneidungen zwischen Tests auf hoher und niedriger Ebene.

Wenn die Umgestaltung der betroffenen Tests nicht zu kostspielig ist, würde ich den letzteren Ansatz bevorzugen. Im wirklichen Leben toleriere ich jedoch beim massiven Refactoring eine kleine Menge an Testcode-Duplikaten, wenn es mir genug Zeit spart :-)

Soll ich auch die Factory-Klasse testen?

Auch hier kommt es darauf an. Beachten Sie, dass die Tests calculateTaxdie Fabrik effektiv testen. Wenn der Factory-Code also ein trivialer switchBlock wie Ihr Code oben ist, sind diese Tests möglicherweise alles, was Sie benötigen. Wenn die Fabrik jedoch einige schwierigere Aufgaben ausführt, sollten Sie einige Tests speziell dafür durchführen. Alles läuft darauf hinaus, wie viele Tests Sie benötigen, um sicher zu sein, dass der betreffende Code wirklich funktioniert. Wenn Sie beim Lesen des Codes oder beim Analysieren von Daten zur Codeabdeckung ungetestete Ausführungspfade sehen, widmen Sie weitere Tests, um diese auszuführen. Wiederholen Sie diesen Vorgang, bis Sie mit Ihrem Code vertraut sind.

Péter Török
quelle
Ich habe den Code ein wenig geändert, um ihn meinem tatsächlichen praktischen Code näher zu bringen. Nun wurde eine zweite Eingabe salaryzur Funktion calculateTax()hinzugefügt. Auf diese Weise werde ich wahrscheinlich den Testcode für die ursprüngliche Funktion und die 3 Implementierungen der Strategieklasse duplizieren.
Songo
@ Songo, siehe mein Update.
Péter Török
5

Ich beginne damit, dass ich kein Experte für TDD oder Unit-Tests bin, aber hier ist, wie ich dies testen würde (ich werde pseudoähnlichen Code verwenden):

CalculateTaxDelegatesToNameHandler()
{
    INameHandlerFactory fakeNameHandlerFactory = Fake(INameHandlerFactory);
    INameHandler fakeNameHandler = Fake(INameHandler);

    A.Call.To(fakeNameHandlerFactory.getHandler("John")).Returns(fakeNameHandler);

    Employee employee = new Employee(fakeNameHandlerFactory);
    employee.CalculateTax("John");

    Assert.That.WasCalled(fakeNameHandler.calculateTax());
}

Also würde ich prüfen , dass die calculateTax()Methode der Mitarbeiterklasse ihre korrekt fragt NameHandlerFactoryfür ein NameHandlerund ruft dann die calculateTax()Methode des zurück NameHandler.

Kristof Claes
quelle
hmmmm Sie meinen also, ich sollte den Test stattdessen zu einem Verhaltenstest machen (testen, ob bestimmte Funktionen aufgerufen wurden) und die Wertaussagen für die delegierten Klassen machen?
Songo
Ja, das würde ich tun. Ich würde in der Tat separate Tests für die NameHandlerFactory und den NameHandler schreiben. Wenn Sie diese haben, gibt es keinen Grund, ihre Funktionalität in der Employee.calculateTax()Methode erneut zu testen . Auf diese Weise müssen Sie keine zusätzlichen Mitarbeitertests hinzufügen, wenn Sie einen neuen NameHandler einführen.
Kristof Claes
3

Sie nehmen an einer Klasse teil (Mitarbeiter, der alles macht) und bilden drei Gruppen von Klassen: die Fabrik, den Mitarbeiter (der nur eine Strategie enthält) und die Strategien.

Machen Sie also 3 Gruppen von Tests:

  1. Testen Sie die Fabrik isoliert. Behandelt es Eingaben korrekt? Was passiert, wenn Sie ein Unbekanntes übergeben?
  2. Testen Sie den Mitarbeiter isoliert. Können Sie eine beliebige Strategie festlegen, die wie erwartet funktioniert? Was passiert, wenn keine Strategie oder Werkseinstellung vorhanden ist? (wenn das im Code möglich ist)
  3. Testen Sie die Strategien isoliert. Führt jeder die Strategie aus, die Sie erwarten? Behandeln sie ungerade Randeingaben auf konsistente Weise?

Sie können natürlich automatisierte Tests für den gesamten Shebang durchführen, aber diese sind jetzt eher Integrationstests und sollten als solche behandelt werden.

Telastyn
quelle
2

Bevor ich Code schreibe, beginne ich mit einem Test für eine Fabrik. Wenn ich mich über das Zeug lustig mache, das ich brauche, würde ich mich zwingen, über die Implementierungen und Verwendungszwecke nachzudenken.

Dann würde ich eine Factory implementieren und mit einem Test für jede Implementierung und schließlich den Implementierungen selbst für diese Tests fortfahren.

Schließlich würde ich die alten Tests entfernen.

Patkos Csaba
quelle
2

Meiner Meinung nach sollten Sie nichts tun, dh Sie sollten keine neuen Tests hinzufügen.

Ich betone, dass dies eine Meinung ist und tatsächlich davon abhängt, wie Sie die Erwartungen an das Objekt wahrnehmen. Denken Sie, dass der Benutzer der Klasse eine Strategie für die Steuerberechnung liefern möchte? Wenn es ihn nicht interessiert, sollten die Tests dies widerspiegeln, und das Verhalten, das sich aus den Komponententests ergibt, sollte sein, dass es ihnen egal sein sollte, dass die Klasse begonnen hat, ein Strategieobjekt zur Berechnung der Steuer zu verwenden.

Ich bin tatsächlich mehrmals auf dieses Problem gestoßen, als ich TDD verwendet habe. Ich denke, der Hauptgrund ist, dass ein Strategieobjekt keine natürliche Abhängigkeit ist, im Gegensatz zu einer Abhängigkeit von Architekturgrenzen wie einer externen Ressource (einer Datei, einer Datenbank, einem Remotedienst usw.). Da es sich nicht um eine natürliche Abhängigkeit handelt, stütze ich das Verhalten meiner Klasse normalerweise nicht auf diese Strategie. Mein Instinkt ist, dass ich meine Tests nur ändern sollte, wenn sich die Erwartungen an meine Klasse geändert haben.

Es gibt einen großartigen Beitrag von Onkel Bob, der genau über dieses Problem bei der Verwendung von TDD spricht.

Ich denke, dass die Tendenz, jede einzelne Klasse zu testen, TDD tötet. Das Schöne an TDD ist, dass Sie Tests verwenden, um Designschemata voranzutreiben, und nicht umgekehrt.

Rafi Goldfarb
quelle