Sollten die erwarteten Ergebnisse des Komponententests fest codiert werden?

29

Sollten die erwarteten Ergebnisse eines Komponententests fest codiert werden, oder können sie von initialisierten Variablen abhängen? Erhöhen hartcodierte oder berechnete Ergebnisse das Risiko von Fehlern im Komponententest? Gibt es andere Faktoren, die ich nicht berücksichtigt habe?

Welches dieser beiden Formate ist beispielsweise zuverlässiger?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

EDIT 1: Ist Option 3 als Antwort auf die Antwort von DXM eine bevorzugte Lösung?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Hand-E-Food
quelle
2
Tue beides. Ernst. Tests können und sollten sich überschneiden. Sehen Sie sich auch einige datengesteuerte Tests an, wenn Sie mit fest codierten Werten zu tun haben.
Job
Ich würde zustimmen, dass die dritte Option das ist, was ich gerne benutze. Ich glaube nicht, dass die Option 1 schaden würde, da Sie die Manipulation beim Kompilieren eliminieren.
Kwelch
Ihre beiden Optionen verwenden jedoch Hardcoding und brechen ab, wenn der Test nicht mit C: \\
Qwertie,

Antworten:

27

Ich denke, berechnete Erwartungswerte ergeben robustere und flexiblere Testfälle. Auch durch die Verwendung guter Variablennamen in dem Ausdruck, der das erwartete Ergebnis berechnet, wird viel klarer, woher das erwartete Ergebnis überhaupt stammt.

Abgesehen davon würde ich in Ihrem speziellen Beispiel der "Softcoded" -Methode NICHT vertrauen, da sie Ihr zu testendes SUT (System) als Eingabe für Ihre Berechnungen verwendet. Wenn in MyClass ein Fehler auftritt, bei dem Felder nicht ordnungsgemäß gespeichert werden, besteht der Test tatsächlich, da bei der Berechnung des erwarteten Werts genau wie bei target.GetPath () die falsche Zeichenfolge verwendet wird.

Mein Vorschlag wäre, den erwarteten Wert dort zu berechnen, wo es Sinn macht, aber sicherzustellen, dass die Berechnung nicht von einem Code des SUT selbst abhängt.

Als Antwort auf die Aktualisierung von OP auf meine Antwort:

Ja, aufgrund meines Wissens, aber der begrenzten Erfahrung mit TDD, würde ich Option 3 wählen.

DXM
quelle
1
Guter Punkt! Verlassen Sie sich im Test nicht auf das nicht verifizierte Objekt.
Hand-E-Food
ist es nicht eine Vervielfältigung des SUT-Codes?
Abyx
1
In gewisser Weise ist es das, aber so stellen Sie sicher, dass SUT funktioniert. Wenn wir den gleichen Code verwenden würden und er kaputt gehen würde, würden Sie nie erfahren. Wenn Sie zum Durchführen der Berechnung viel SUT duplizieren müssen, ist Option 1 möglicherweise besser, wenn Sie den Wert nur hart codieren.
DXM
16

Was ist, wenn der Code wie folgt lautet:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Ihr zweites Beispiel würde den Fehler nicht erkennen, das erste Beispiel jedoch.

Im Allgemeinen würde ich gegen Soft-Codierung empfehlen, da es Fehler verbergen kann. Beispielsweise:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

Können Sie das Problem erkennen? In einer hartkodierten Version würden Sie nicht denselben Fehler machen. Es ist schwieriger, die Berechnungen korrekt zu machen als hartcodierte Werte. Deshalb arbeite ich lieber mit hartcodierten Werten als mit weichcodierten.

Aber es gibt Ausnahmen. Was ist, wenn Ihr Code unter Windows und Linux ausgeführt werden muss? Der Pfad muss nicht nur unterschiedlich sein, er muss auch unterschiedliche Pfadtrennzeichen verwenden! Die Berechnung des Pfads mithilfe von Funktionen, die den Unterschied abstrahieren, kann in diesem Kontext sinnvoll sein.

Winston Ewert
quelle
Ich höre, was Sie sagen, und das gibt mir etwas zu bedenken. Die Softcodierung basiert auf der Übergabe meiner anderen Testfälle (wie z. B. ConstructorShouldCorrectlyInitialiseFields). Der von Ihnen beschriebene Fehler wird von anderen fehlgeschlagenen Komponententests referenziert.
Hand-E-Food
@ Hand-E-Food, es klingt so, als würden Sie Tests zu einzelnen Methoden Ihrer Objekte schreiben. Nicht. Sie sollten Tests schreiben, die die Korrektheit Ihres gesamten Objekts überprüfen, nicht einzelne Methoden. Andernfalls werden Ihre Tests in Bezug auf Änderungen im Objekt spröde.
Winston Ewert
Ich bin nicht sicher, ob ich folge. Das Beispiel, das ich gegeben habe, war rein hypothetisch, ein leicht zu verstehendes Szenario. Ich schreibe Unit-Tests, um öffentliche Mitglieder von Klassen und Objekten zu testen. Ist das die richtige Art, sie zu benutzen?
Hand-E-Food
@ Hand-E-Food, wenn ich Sie richtig verstehe, würde Ihr Test ConstructShouldCorrectlyInitialiseFields den Konstruktor aufrufen und dann bestätigen, dass die Felder richtig gesetzt sind. Aber das solltest du nicht tun. Es sollte Ihnen egal sein, was die internen Felder tun. Sie sollten nur behaupten, dass das externe Verhalten des Objekts korrekt ist. Andernfalls kann der Tag kommen, an dem Sie die interne Implementierung ersetzen müssen. Wenn Sie Aussagen über den internen Status gemacht haben, brechen alle Ihre Tests ab. Wenn Sie jedoch nur Aussagen zum externen Verhalten gemacht haben, funktioniert alles weiterhin.
Winston Ewert
@ Winston - Ich bin gerade dabei, das Buch mit den xUnit-Testmustern zu durchforsten und habe vorher die Kunst des Komponententests abgeschlossen. Ich werde nicht so tun, als ob ich wüsste, wovon ich spreche, aber ich würde gerne glauben, dass ich etwas aus diesen Büchern mitgenommen habe. In beiden Büchern wird nachdrücklich empfohlen, dass jede Testmethode ein absolutes Minimum testet und dass Sie viele Testfälle haben sollten, um Ihr gesamtes Objekt zu testen. Auf diese Weise sollten Sie bei Änderungen an Schnittstellen oder Funktionen nur mit wenigen Testmethoden rechnen und nicht mit den meisten. Und da sie klein sind, sollten Änderungen einfacher sein.
DXM
4

Meiner Meinung nach sind Ihre beiden Vorschläge alles andere als ideal. Der ideale Weg, dies zu tun, ist dieser:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Mit anderen Worten, der Test sollte ausschließlich auf der Grundlage der Eingabe und der Ausgabe des Objekts und nicht auf der Grundlage des internen Zustands des Objekts funktionieren. Das Objekt sollte als Blackbox behandelt werden. (Ich ignoriere andere Probleme, wie die Unangemessenheit der Verwendung von string.Join anstelle von Path.Combine, da dies nur ein Beispiel ist.)

Mike Nakis
quelle
1
Nicht alle Methoden sind funktionsfähig - viele haben korrekterweise Nebenwirkungen, die den Status einiger Objekte oder Objekte ändern. Bei einem Komponententest für eine Methode mit Nebenwirkungen müsste wahrscheinlich der Zustand der von der Methode betroffenen Objekte bewertet werden.
Matthew Flynn
Dann würde dieser Zustand als Ausgabe der Methode betrachtet. Mit diesem Beispieltest soll die GetPath () -Methode und nicht der Konstruktor von MyClass überprüft werden. Lesen Sie die Antwort von @ DXM, er liefert einen sehr guten Grund für den Black-Box-Ansatz.
Mike Nakis
@MatthewFlynn, dann sollten Sie die von diesem Status betroffenen Methoden testen. Der genaue interne Status ist ein Implementierungsdetail und geht den Test nichts an.
Winston Ewert
@MatthewFlynn, nur um das zu verdeutlichen, bezieht sich das auf das gezeigte Beispiel oder etwas anderes, das für andere Unit-Tests in Betracht gezogen werden muss? Ich konnte sehen, dass das für etwas von Bedeutung ist target.Dispose(); Assert.IsTrue(target.IsDisposed);(ein sehr einfaches Beispiel.)
Hand-E-Food
Auch in diesem Fall ist (oder sollte) die IsDisposed-Eigenschaft ein unverzichtbarer Teil der öffentlichen Schnittstelle der Klasse und kein Implementierungsdetail. (Die IDispose-Schnittstelle bietet keine solche Eigenschaft, aber das ist bedauerlich.)
Mike Nakis
2

Es gibt zwei Aspekte in der Diskussion:

1. Verwenden des Ziels selbst für den Testfall
Die erste Frage ist, ob Sie die Klasse selbst verwenden können, um sich auf die im Teststub geleistete Arbeit zu verlassen und einen Teil davon zu erhalten. - Die Antwort lautet NEIN, da Sie im Allgemeinen niemals eine Annahme über den Code machen sollten, den Sie testen. Wenn dies nicht richtig gemacht wird, werden Bugs im Laufe der Zeit immun gegen einige Unit-Tests.

2. Hardcoding
sollten Sie hart codieren ? Wieder lautet die Antwort Nein . Denn wie bei jeder Software wird die harte Kodierung der Informationen schwierig, wenn sich die Dinge weiterentwickeln. Wenn Sie beispielsweise möchten, dass der oben genannte Pfad erneut geändert wird, müssen Sie entweder eine zusätzliche Einheit schreiben oder die Änderung fortsetzen. Eine bessere Methode besteht darin, das Eingabe- und Auswertungsdatum aus der separaten Konfiguration abzuleiten, die einfach angepasst werden kann.

Zum Beispiel ist hier, wie ich den Teststummel richtig machen würde.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Dipan Mehta
quelle
0

Es sind viele Konzepte möglich, die anhand einiger Beispiele den Unterschied erkennen lassen

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Um es zusammenzufassen: Im Allgemeinen ist Ihr erster, nur fest codierter Test für mich am sinnvollsten, da er einfach ist, direkt auf den Punkt kommt usw. Wenn Sie einen Pfad zu oft fest codieren, geben Sie ihn einfach in die Setup-Methode ein.

Für zukünftige strukturierte Tests würde ich Datenquellen auschecken, sodass Sie einfach weitere Datenzeilen hinzufügen können, wenn Sie mehr Testsituationen benötigen.

Luc Franken
quelle
0

Moderne Test-Frameworks ermöglichen es Ihnen, Parameter für Ihre Methode bereitzustellen. Ich würde diese nutzen:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Dies hat aus meiner Sicht mehrere Vorteile:

  1. Entwickler sind oft versucht, die scheinbar einfachen Teile des Codes aus ihrem SUT in ihre Komponententests zu kopieren. Wie Winston hervorhebt , können in diesen immer noch knifflige Fehler versteckt sein. "Hardcodierung" Das erwartete Ergebnis hilft dabei, Situationen zu vermeiden, in denen Ihr Testcode aus dem gleichen Grund falsch ist wie Ihr ursprünglicher Code. Wenn Sie jedoch aufgrund geänderter Anforderungen gezwungen sind, hartcodierte Zeichenfolgen aufzuspüren, die in Dutzenden von Testmethoden eingebettet sind, kann dies ärgerlich sein. Wenn Sie alle fest programmierten Werte außerhalb Ihrer Testlogik an einem Ort haben, haben Sie das Beste aus beiden Welten.
  2. Sie können Tests für verschiedene Eingaben und erwartete Ausgaben mit einer einzigen Codezeile hinzufügen. Dies ermutigt Sie, mehr Tests zu schreiben, während Sie Ihren Testcode trocken halten und einfach warten können. Ich finde, weil es so billig ist, Tests hinzuzufügen, bin ich offen für neue Testfälle, an die ich nicht gedacht hätte, wenn ich eine ganz neue Methode für sie schreiben müsste. Welches Verhalten würde ich beispielsweise erwarten, wenn einer der Eingänge einen Punkt enthält? Ein Backslash? Was wäre, wenn einer leer wäre? Oder Leerzeichen? Oder mit Leerzeichen begonnen oder geendet?
  3. Das Test-Framework behandelt jeden Testfall als seinen eigenen Test und fügt sogar die bereitgestellten Ein- und Ausgaben in den Testnamen ein. Wenn alle Testfälle bis auf einen bestehen, ist es sehr einfach zu erkennen, welche kaputt sind und wie sie sich von allen anderen unterschieden.
StriplingWarrior
quelle