Benötige ich wirklich ein Unit-Test-Framework?

19

Derzeit haben wir in meinem Job eine große Reihe von Komponententests für unsere C ++ - Anwendung. Wir verwenden jedoch kein Unit-Test-Framework. Sie verwenden einfach ein C-Makro, das im Grunde ein Assert und ein Cout umschließt. So etwas wie:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

Dann erstellen wir einfach Funktionen für jeden unserer Tests wie

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

Unser CI-Server erkennt "Komponententest fehlgeschlagen" und schlägt beim Erstellen fehl und sendet die Nachricht per E-Mail an die Entwickler.

Und wenn wir doppelten Setup-Code haben, überarbeiten wir ihn einfach wie jeden anderen duplizierten Code, den wir in der Produktion haben würden. Wir packen es hinter Hilfsfunktionen, lassen einige Testklassen einpacken und häufig verwendete Szenarien einrichten.

Ich weiß, dass es Frameworks wie CppUnit und Boost Unit Test gibt. Ich frage mich, welchen Wert diese hinzufügen? Vermisse ich, was diese auf den Tisch bringen? Gibt es etwas Nützliches, das ich von ihnen gewinnen könnte? Ich zögere, eine Abhängigkeit hinzuzufügen, es sei denn, sie schafft einen echten Mehrwert, zumal das, was wir haben, einfach zu sein scheint und gut funktioniert.

Doug T.
quelle

Antworten:

8

Wie andere bereits gesagt haben, haben Sie bereits Ihr eigenes, einfaches, hausgemachtes Framework.

Es scheint trivial zu sein, einen zu machen. Es gibt jedoch einige andere Funktionen eines Unit-Test-Frameworks, die nicht so einfach zu implementieren sind, da sie fortgeschrittene Sprachkenntnisse voraussetzen. Die Funktionen, die ich in der Regel von einem Test-Framework benötige und die nicht so einfach zu erstellen sind, sind:

  • Automatische Erfassung von Testfällen. Das heißt, die Definition einer neuen Testmethode sollte ausreichen, um sie auszuführen. JUnit sammelt automatisch alle Methoden, deren Namen mit " testund" beginnen. NUnit enthält die [Test]Annotation. Boost.Test verwendet die Makros " BOOST_AUTO_TEST_CASEund" BOOST_FIXTURE_TEST_CASE.

    Es ist größtenteils Bequemlichkeit, aber jedes bisschen Bequemlichkeit, das Sie bekommen können, verbessert die Chance, dass Entwickler tatsächlich die Tests schreiben, die sie sollten, und dass sie sie korrekt anschließen. Wenn Sie lange Anweisungen haben, wird jemand einen Teil von ihnen jetzt verpassen und dann werden möglicherweise einige Tests nicht ausgeführt und niemand wird es bemerken.

  • Möglichkeit, ausgewählte Testfälle auszuführen, ohne den Code zu ändern und neu zu kompilieren. In jedem vernünftigen Unit-Test-Framework können Sie angeben, welche Tests auf der Befehlszeile ausgeführt werden sollen. Wenn Sie Unit-Tests debuggen möchten (dies ist für viele Entwickler der wichtigste Punkt in ihnen), müssen Sie in der Lage sein, nur einige auszuwählen, die ausgeführt werden sollen, ohne den Code überall zu ändern.

    Angenommen, Sie haben gerade den Fehlerbericht Nr. 4211 erhalten, der mit dem Komponententest reproduziert werden kann. Sie schreiben also eine, müssen aber dann dem Läufer mitteilen, dass er nur diesen Test ausführen soll, damit Sie debuggen können, was dort tatsächlich falsch ist.

  • Möglichkeit, erwartete Fehler pro Testfall zu markieren, ohne die Prüfungen selbst zu ändern. Wir haben bei der Arbeit tatsächlich das Framework gewechselt, um dieses zu bekommen.

    Jede Test-Suite mit angemessener Größe wird Tests haben, die fehlschlagen, weil die von ihnen getesteten Funktionen noch nicht implementiert wurden, noch nicht fertiggestellt wurden, niemand Zeit hatte, sie zu reparieren oder so. Ohne die Möglichkeit, Tests als erwartete Fehler zu markieren, werden Sie bei regelmäßigen Fehlern keinen weiteren Fehler bemerken, sodass die Tests ihren Hauptzweck nicht mehr erfüllen.

Jan Hudec
quelle
danke ich denke das ist die beste antwort. Momentan macht mein Makro seine Arbeit, aber ich kann keine der von Ihnen erwähnten Funktionen ausführen.
Doug T.
1
@Jan Hudec "Meistens ist es Bequemlichkeit, aber jedes bisschen Bequemlichkeit, das Sie bekommen können, verbessert die Chance, dass Entwickler tatsächlich die Tests schreiben, die sie sollten, und dass sie sie korrekt anschließen."; Alle Test-Frameworks sind (1) nicht einfach zu installieren, enthalten häufig mehr veraltete oder nicht vollständige Installationsanweisungen als aktuelle gültige Anweisungen. (2) Wenn Sie sich direkt auf ein Testframework festlegen, ohne eine Schnittstelle in der Mitte, sind Sie damit verheiratet. Das Wechseln des Frameworks ist nicht immer einfach.
Dmitry
@Jan Hudec Wenn wir erwarten, dass mehr Leute Unit-Tests schreiben, müssen wir bei Google mehr Ergebnisse für "Was ist ein Unit-Test" haben als für "Was ist Unit-Test". Es macht keinen Sinn, Komponententests durchzuführen, wenn Sie keine Ahnung haben, was ein Komponententest unabhängig von einem Komponententest-Framework oder einer Definition von Komponententests ist. Sie können Unit-Tests nur durchführen, wenn Sie genau wissen, was ein Unit-Test ist, da es sonst keinen Sinn macht, Unit-Tests durchzuführen.
Dmitry
Ich kaufe dieses Convenience-Argument nicht. Das Schreiben von Testcode ist sehr schwierig, wenn Sie die triviale Welt der Beispiele verlassen. Für all diese Modelle, Setups, Bibliotheken, externen Modellserverprogramme usw. ist es erforderlich, dass Sie das Testframework von Grund auf kennen.
Lothar
@Lothar, ja, es ist alles eine Menge Arbeit und viel zu lernen, aber immer wieder ein einfaches Boilerplate schreiben zu müssen, da es an einigen nützlichen Hilfsprogrammen mangelt, macht die Arbeit viel weniger angenehm und das macht einen spürbaren Unterschied in der Effektivität.
Jan Hudec
27

Scheint, als ob Sie bereits ein Framework verwenden, ein hausgemachtes.

Was ist der Mehrwert von populäreren Frameworks? Ich würde sagen, dass der Mehrwert darin besteht, dass Sie Code mit Personen außerhalb Ihres Unternehmens austauschen können, da er auf dem bekannten und weit verbreiteten Framework basiert .

Ein selbst erstelltes Framework hingegen zwingt Sie dazu, entweder Ihren Code niemals freizugeben oder das Framework selbst bereitzustellen, was mit dem Wachstum des Frameworks selbst umständlich werden kann.

Wenn Sie einem Kollegen Ihren Code so wie er ist, ohne Erklärung und ohne Unit-Test-Framework, geben, ist er nicht in der Lage, ihn zu kompilieren.

Ein zweiter Nachteil von hausgemachten Frameworks ist die Kompatibilität . Gängige Unit-Test-Frameworks stellen in der Regel die Kompatibilität mit verschiedenen IDEs, Versionskontrollsystemen usw. sicher. Im Moment ist dies möglicherweise nicht sehr wichtig für Sie, aber was passiert, wenn Sie eines Tages etwas auf Ihrem CI-Server ändern oder migrieren müssen zu einer neuen IDE oder einem neuen VCS? Wirst du das Rad neu erfinden?

Zu guter Letzt bieten größere Frameworks mehr Funktionen, die Sie möglicherweise eines Tages in Ihrem eigenen Framework implementieren müssen. Assert.AreEqual(expected, actual)ist nicht immer genug. Was ist, wenn Sie Folgendes benötigen:

  • Präzision messen?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • ungültiger Test, wenn er zu lange läuft? Das erneute Implementieren eines Timeouts ist möglicherweise selbst in Sprachen, die eine asynchrone Programmierung ermöglichen, nicht einfach.

  • Eine Methode testen, bei der eine Ausnahme erwartet wird?

  • Haben Sie einen eleganteren Code?

    Assert.Verify(a == null);
    

    ist in Ordnung, aber ist es nicht ausdrucksvoller für Ihre Absicht, stattdessen die nächste Zeile zu schreiben?

    Assert.IsNull(a);
    
Arseni Mourzenko
quelle
Das von uns verwendete "Framework" befindet sich alle in einer sehr kleinen Header-Datei und folgt der Semantik von assert. Ich mache mir also keine Sorgen über die Nachteile, die Sie auflisten.
Doug T.
4
Ich halte die Behauptungen für den trivialsten Teil des Test-Frameworks. Der Läufer, der die Testfälle sammelt und ausführt und die Ergebnisse überprüft, ist der nicht unbedeutende wichtige Teil.
Jan Hudec
@Jan ich folge nicht ganz. Mein Läufer ist eine Hauptroutine, die jedem C ++ - Programm gemeinsam ist. Tut ein Unit-Test-Framework-Läufer etwas Anspruchsvolleres und Nützlicheres?
Doug T.
1
Ihr Framework erlaubt nur die Semantik des Asserts und der Ausführung von Tests in einer Hauptmethode ... bis jetzt. Warten Sie einfach, bis Sie Ihre Zusicherungen in mehrere Szenarien gruppieren müssen, gruppieren Sie zusammengehörige Szenarien basierend auf initialisierten Daten usw.
James Kingsbery
@DougT .: Ja, ein anständiger Unit-Test-Framework-Läufer macht einige raffiniertere nützliche Dinge. Siehe meine vollständige Antwort.
Jan Hudec
4

Wie andere bereits gesagt haben, haben Sie bereits Ihr eigenes, hausgemachtes Framework.

Der einzige Grund, warum ich andere Test-Frameworks verwenden kann, ist aus Sicht der Branche "allgemein bekannt". Neue Entwickler müssten nicht lernen, wie man es zu Hause macht (obwohl es sehr einfach aussieht).

Andere Test-Frameworks bieten möglicherweise weitere Funktionen, die Sie nutzen können.

ozz
quelle
1
Einverstanden. Wenn Sie mit Ihrer aktuellen Teststrategie nicht an Grenzen stoßen, sehe ich wenig Grund, dies zu ändern. Ein gutes Framework würde wahrscheinlich bessere Organisations- und Berichtsfunktionen bieten, aber Sie müssten den zusätzlichen Aufwand für die Integration in Ihre Codebasis (einschließlich Ihres Buildsystems) rechtfertigen.
TMN
3

Sie haben bereits ein Framework, auch wenn es ein einfaches ist.

Die Hauptvorteile eines größeren Frameworks, wie ich sie sehe, sind die Fähigkeit, viele verschiedene Arten von Behauptungen zu haben (wie das Behaupten von Erhöhungen), eine logische Reihenfolge für die Komponententests und die Fähigkeit, nur eine Teilmenge von Komponententests unter auszuführen eine Zeit. Auch das Muster von xUnit-Tests ist recht gut zu befolgen, wenn Sie können - zum Beispiel das von setUP () und tearDown (). Das sperrt Sie natürlich in diesen Rahmen. Beachten Sie, dass einige Frameworks eine bessere Scheinintegration aufweisen als andere - zum Beispiel google mock and test.

Wie lange dauert es, bis Sie alle Unit-Tests auf ein neues Framework umgestellt haben? Tage oder ein paar Wochen sind es vielleicht wert, aber mehr vielleicht nicht so sehr.

Sardathrion - Setzen Sie Monica wieder ein
quelle
2

So wie ich das sehe, habt ihr beide den Vorteil und seid im "Nachteil".

Der Vorteil ist, dass Sie ein System haben, mit dem Sie sich wohl fühlen und das für Sie funktioniert. Sie sind froh, dass dies die Gültigkeit Ihres Produkts bestätigt, und Sie würden wahrscheinlich keinen geschäftlichen Nutzen daraus ziehen, zu versuchen, alle Ihre Tests für etwas zu ändern, das ein anderes Framework verwendet. Wenn Sie Ihren Code umgestalten können und Ihre Tests die Änderungen übernehmen - oder noch besser, wenn Sie Ihre Tests ändern können und Ihr vorhandener Code die Tests nicht besteht, bis er umgestaltet wird, sind alle Grundlagen abgedeckt. Jedoch...

Einer der Vorteile einer gut gestalteten Unit-Testing-API besteht darin, dass die meisten modernen IDEs viel native Unterstützung bieten. Dies hat keine Auswirkungen auf das Hardcore-VI und die Emac-Benutzer, die sich über die Visual Studio-Benutzer lustig machen. Für diejenigen, die eine gute IDE verwenden, haben Sie jedoch die Möglichkeit, Ihre Tests zu debuggen und innerhalb dieser auszuführen die IDE selbst. Dies ist gut, jedoch gibt es je nach verwendetem Framework einen noch größeren Vorteil, und zwar in der Sprache, die zum Testen Ihres Codes verwendet wird.

Wenn ich Sprache sage , spreche ich nicht von einer Programmiersprache, sondern von einem umfangreichen Satz von Wörtern, die in einer fließenden Syntax zusammengefasst sind, die Testcode wie eine Geschichte lesen lässt. Insbesondere bin ich ein Verfechter der Verwendung von BDD- Frameworks geworden. Meine persönliche Lieblings-DotNet BDD- API ist StoryQEs gibt jedoch mehrere andere, die denselben grundlegenden Zweck haben: ein Konzept aus einem Anforderungsdokument herauszunehmen und es in Code zu schreiben, ähnlich wie es in der Spezifikation geschrieben ist. Die wirklich guten APIs gehen jedoch noch weiter, indem sie jede einzelne Anweisung innerhalb eines Tests abfangen und angeben, ob diese Anweisung erfolgreich ausgeführt wurde oder fehlgeschlagen ist. Dies ist unglaublich nützlich, da Sie den gesamten Test sehen können, der ausgeführt wird, ohne vorzeitig zurückzukehren. Dies bedeutet, dass Ihre Debug-Bemühungen unglaublich effizient werden, da Sie sich nur auf die Teile des Tests konzentrieren müssen, die fehlgeschlagen sind, ohne den gesamten Aufruf entschlüsseln zu müssen Sequenz. Die andere nette Sache ist, dass die Testausgabe Ihnen alle diese Informationen zeigt,

Als Beispiel für das, wovon ich spreche, vergleiche Folgendes:

Verwenden von Asserts:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

Verwenden einer fließenden BDD-API: (Stellen Sie sich vor, die kursiven Bits sind im Grunde Methodenzeiger.)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

Nun ist die BDD-Syntax zwar länger und wortreicher, und diese Beispiele sind schrecklich erfunden. Für sehr komplexe Testsituationen, in denen sich aufgrund eines bestimmten Systemverhaltens eine Menge Dinge in einem System ändern, bietet Ihnen die BDD-Syntax jedoch Klarheit Beschreibung, was Sie testen und wie Ihre Testkonfiguration definiert wurde, und Sie können diesen Code einem Nicht-Programmierer zeigen, und er wird sofort verstehen, was los ist. Wenn "variable_A" in beiden Fällen den Test nicht besteht, wird das Asserts-Beispiel erst nach dem ersten Assert ausgeführt, nachdem Sie das Problem behoben haben. Die BDD-API führt nacheinander alle in der Kette aufgerufenen Methoden aus und gibt an, welche einzelne Teile der Aussage waren fehlerhaft.

Persönlich finde ich, dass dieser Ansatz viel besser funktioniert als die herkömmlicheren xUnit-Frameworks in dem Sinne, dass die Testsprache dieselbe Sprache ist, in der Ihre Kunden über ihre logischen Anforderungen sprechen. Trotzdem habe ich es geschafft, xUnit-Frameworks in einem ähnlichen Stil zu verwenden, ohne eine vollständige Test-API erfinden zu müssen, um meine Bemühungen zu unterstützen, und obwohl die Behauptungen sich selbst effektiv kurzschließen, lesen sie sich sauberer. Zum Beispiel:

Mit Nunit :

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

Wenn Sie sich für die Verwendung einer Unit-Testing-API entscheiden, empfehle ich, einige Zeit mit einer großen Anzahl verschiedener APIs zu experimentieren und Ihren Ansatz offen zu halten. Während ich persönlich für BDD eintrete, erfordern Ihre eigenen Geschäftsanforderungen möglicherweise etwas anderes für die Umstände Ihres Teams. Der Schlüssel ist jedoch zu vermeiden, dass Sie Ihr bestehendes System hinterfragen. Sie können Ihre vorhandenen Tests jederzeit mit ein paar Tests unter Verwendung einer anderen API unterstützen, aber ich würde auf keinen Fall empfehlen, einen umfangreichen Test neu zu schreiben, nur um alles gleich zu machen. Da veralteter Code nicht mehr verwendet wird, können Sie ihn und seine Tests einfach durch neuen Code und Tests mit einer alternativen API ersetzen, ohne dass Sie in einen großen Aufwand investieren müssen, der Ihnen nicht unbedingt einen echten geschäftlichen Nutzen bringt. Wie für die Verwendung einer Unit-Testing-API,

S.Robins
quelle
1

Was Sie haben, ist einfach und erledigt die Arbeit. Wenn es bei Ihnen funktioniert, großartig. Sie nicht brauchen einen Mainstream - Unit Testing Framework, und ich würde auf die Arbeit der Portierung eine bestehende Bibliothek von Unit - Tests zu einem neuen Rahmen gehen zögern. Ich denke, der größte Wert von Unit-Testing-Frameworks besteht darin, die Markteintrittsbarriere zu verringern. Sie schreiben gerade Tests, weil das Framework bereits vorhanden ist. Sie haben diesen Punkt überschritten, sodass Sie diesen Vorteil nicht erhalten.

Der andere Vorteil der Verwendung eines Mainstream-Frameworks - und es ist ein kleiner Vorteil, IMO - besteht darin, dass neue Entwickler möglicherweise bereits mit dem von Ihnen verwendeten Framework auf dem neuesten Stand sind und daher weniger Schulung benötigen. In der Praxis sollte dies mit einem einfachen Ansatz wie dem, den Sie beschrieben haben, keine große Sache sein.

Außerdem haben die meisten Mainstream-Frameworks bestimmte Funktionen, die Ihr Framework möglicherweise aufweist oder nicht. Diese Funktionen reduzieren den Installationscode und machen das Schreiben von Testfällen schneller und einfacher:

  • Automatische Ausführung von Testfällen unter Verwendung von Namenskonventionen, Anmerkungen / Attributen usw.
  • Verschiedene spezifischere Zusicherungen, damit Sie nicht für alle Zusicherungen Bedingungslogik schreiben oder Ausnahmen abfangen müssen, um ihren Typ zu bestätigen.
  • Kategorisierung von Testfällen, damit Sie leicht Teilmengen davon ausführen können.
Adam Jaskiewicz
quelle