Test-Driven-Development Best Practices

In diesem Dokument gehe ich davon aus, dass die Grundlagen von Unittest bekannt sind und bekannt ist wie man im genutzten Framework Unittests schreibt. Die Beispiele in diesem Dokument sind Java geschrieben worden, können aber in jede andere Sprache übersetzt werden. Die hier beschriebenen Best Practices sind unabhängig von der genutzten Programmiersprache und gelten generell.

Es ist erschreckend einfach schlechte Unittests zu schreiben, die einem Projekt wenig Mehrwert bringen, die Kosten für Änderungen (costs of code changes) an der Code Basis aber astronomisch werden lassen.

ein Beispiel zu Beginn

Tests für das Beispiel

1. Unittests verhindern keine Bugs

In computer programming, unit testing is a software testing method by which individual units of source code—sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures—are tested to determine whether they are fit for use.
Unittests beweisen nicht die Abwesenheit von Fehlern, sondern nur deren Anwesenheit bei fehlschlagenden Tests.

Es ist wichtig die Motivation hinter Unit testing zu verstehen. Unittests sind kein Weg Bugs in einer Applikation zu finden oder zu verhindern. Per Definition untersucht ein Unittest eine Code-Unit unabhängig und separat. Wenn die Applikation gestartet wird, müssen diese Code-Unit zusammen arbeiten und erst im Zusammenspiel der Code-Unit treten Fehler auf. Mithilfe von Unittest können keine Regression-Bugs gefunden werden. Sicherstellen, dass eine Komponente X und Y unabhängig voneinander korrekt, Unittests sind ein

Test-driven development (TDD) is a software development process relying on software requirements being converted to test cases before software is fully developed, and tracking all software development by repeatedly testing the software against all test cases. This is as opposed to software being developed first and test cases created later.

Zur Findung von Regression Bugs empfiehlt sich Integration testing zu nutzen.

2. Tipps um gute Unittest zu schreiben

2.1. Immer nur eine Code-Unit testen

Wenn eine Code-Unit getestet wird, kann diese mehrere Use Cases haben. Jeder Use Case sollte in separatem Test behandelt werden. Jeder Test muss unabhängig von anderen Tests sein.

Code Beispiel

Test Code Beispiel

Diese Tests helfen bei Code-Änderungen oder Refactoring, die nicht die funktionalität der Tests betreffen. Ein Ausführen der Tests reicht, um weiterhin eine funktionierende Applikation zu liefern. Zusätzlich führt eine Änderung am Verhalten der Businesslogik dazu, dass einer (oder mehrere) Tests fehlschlagen.

2.2. Auf unnötige Assertions verzichten

https://howtodoinjava.com/best-practices/unit-testing-best-practices-junit-reference-guide/

Unittests sind dafür gedacht ein spezielles Verhalten zu abzudecken, nicht eine ganze Liste von Beobachtungen, welche in dem Code geschehen.

Versucht nicht alles auf einmal sicherzustellen, fokussiert euch auf das was ihr testet. Andernfalls werdet ihr bei einer kleinen Codeänderung mehrere fehlschlagende Tests aufgrund des gleichen Grundes bekommen. Damit erreicht ihr auf längere Sicht nichts.

2.3. Macht jeden Test unabhängig von allen anderen

Macht keine Kette von Unittests.

Es wird verhindern, dass ihr den Hauptgrund für den Fehler findet und ihr den Code debuggen müsst. Ausserdem erzeugt es Abhängigkeiten, d.h. ihr müsst nach dem ihr 1 Test ändert auch alle darauf aufbauende Tests anpassen.

Wenn möglich benutzt @BeforeEach und @AfterEach bzw. @BeforeAll und @AfterAll als Vorbereitung (wenn nötig) für jeden Test. Wenn ihr mehrere verschiedene Dinge für verschiedene Tests vorbereiten müsst, ist es sinnvoll die Tests in verschieden Klassen zu schieben.

2.4. Simuliert (mocked) alle externen Dienste

Andernfalls testet ihr das Verhalten dieser Dienste mit. Sollte diese Dienste nur online erreichbar sein, funktionieren eure Unittests auch nur online und offline arbeiten ist nicht mehr möglich. Ausserdem können sich durch Status- oder Datenänderungen verschiedene Unittests gegenseitig beeinflussen, was zu falschen fehlschlägen führt.

(Btw. macht es keinen Spass einen Unittest debuggen zu müssen, nur weil ein externer Dienst einen Fehler hat.)

2.5. Tested keine Konfiguration

Per Definition sind Konfiguration nicht Teil des Codes, darum lagern wir sie schließlich in eigene Dateien und in Zeiten von Cloud-Computing in Configuration-Stores aus. Das wichtigste, Konfiguration werden sich zur Laufzeit oder zur Startzeit der Applikation unterscheiden, daher wäre ein Test sinnfrei.

2.6. Benenne deine Unit-Tests klar und einheitlich

Nun, das ist vielleicht der wichtigste Punkt, an den du dich erinnern und dem du weiter folgen solltest. Du musst deine Testfälle danach benennen, was sie tatsächlich tun und testen. Eine Namenskonvention für Testfälle, die Klassennamen und Methodennamen für Testfallnamen verwendet, ist niemals eine gute Idee. Jedes Mal, wenn du den Methoden- oder Klassennamen änderst, wirst du am Ende auch viele Testfälle aktualisieren. Bei Refactoring sollten Änderungen am Test-Code minimal sein.

Wenn deine Testfallnamen jedoch logisch sind, d.h. auf Operationen basieren, müssen Sie fast keine Änderungen vornehmen, da die Anwendungslogik höchstwahrscheinlich gleich bleibt.

Z.B. Testfallnamen sollten wie folgt aussehen:

  • create_employee_with_valid_id
  • create_employee_with_null_id_throws_exception
  • create_employee_with_negative_id_throws_exception
  • create_employee_with_duplicate_id_throws_exception

2.7. Alle Methoden, unabhängig der Sichtbarkeit, sollten korrekte und vollständige Tests haben

Nun, das ist in der Tat umstritten.

Du musst nach den kritischsten Teilen deines Codes suchen und du solltest sie testen, ohne dir Gedanken darüberzumachen, ob sie überhaupt privat sind.

Diese Methoden können bestimmte kritische Algorithmen haben, die von einer oder zwei Klassen aufgerufen werden, aber sie spielen eine wichtige Rolle. Du möchtest sicher sein, dass sie wie vorgesehen funktionieren.

2.8. Verwenden Sie die am besten geeigneten Assertion-Methoden

Es gibt viele Assertion-Methoden, mit denen du in jedem Testfall arbeiten kannst. Verwende die am besten geeignete mit der richtigen Argumentation und Überlegung. Sie sind für einen Zweck da, benutze sie.

2.9. Bringen Sie Assertion-Parameter in die richtige Reihenfolge

Assert-Methoden benötigen normalerweise zwei Parameter. Einer ist der erwartete Wert und der zweite ist der ursprüngliche Wert. Übergib sie nach Bedarf der Reihe nach. Dies hilft bei der korrekten Nachrichtenanalyse, wenn etwas schiefgeht.

2.10. Trenne den Testcode von Produktionscode

Stellen Sie in Ihrem Build-Skript sicher, dass der Testcode nicht mit dem tatsächlichen Quellcode bereitgestellt wird. Es ist Ressourcenverschwendung.

2.11. Erstelle Komponententests, die auf Ausnahmen abzielen

Wenn einige eurer Testfälle erwarten, dass die Ausnahmen von der Anwendung ausgelöst werden. Versuche, das Abfangen einer Ausnahme im Catch-Block zu vermeiden, und versuche die Verwendung der Fail- oder Asset-Methode zu vermeiden.

Wenn eine Methode im Testcode eine Ausnahme auslöst, schreibe keinen catch-Block, nur um die Ausnahme abzufangen und den Testfall nicht zu bestehen. Verwenden Sie stattdessen throws Exception-Anweisung in der Testfall-Deklaration selbst. Ich würde die Verwendung der Exception-Klasse empfehlen und keine bestimmten Unterklassen von Exception verwenden. Dadurch wird auch die Testabdeckung erhöht.

Ausserdem wirft eine Assertion im Regelfall selbst eine Exception im Fehlerfall. Wir wollen den Testcode nicht weiter verwenden daher sind präzise Exception wenig hilfreich.

2.12. Verlasse dich nicht auf indirekte Tests

Gehe nicht davon aus, dass ein bestimmter Testfall auch ein anderes Szenario testet, den dies fügt Mehrdeutigkeit hinzu. Schreibe stattdessen für jedes Szenario einen weiteren expliziten Testfall.

2.13. Integrieren Sie Testfälle mit Build-Skript

Dies sollte selbstverständlich sein. Es ist besser, wenn ihr eure Testfälle mit Build-Skripten integrieren können, damit sie automatisch in deiner Continuous Development Umgebung ausgeführt werden. Dies erhöht die Zuverlässigkeit der Anwendung sowie des Testaufbaus.

2.14. Gebt einen Grund an, wenn ein Test überspringen wird

Ein nicht ausgeführter Test nutzt niemandem etwas. Daher sollte ein Test nur temporär per @Disabled deaktiviert werden. Ihr solltet immer einen Grund für das Abschalten des Testes angeben. Nicht nur für euch selbst, sondern auch für eure Kollegen.

  • @Disabled("temporarily disabled for migration")
  • @Disabled("temporarily disabled to deploy hot fix for bug: #123")

Dauerhaft deaktivierte Test können sicher entfernt werden, deren Nutzen hat sich einfach überlebt und deren Produktionscode mit großer Wahrscheinlichkeit schon geändert.

2.15. Benutzt keine versteckten implizite Tests

Beispiel für einen schlechten Test

Dieses Antipattern sehe ich in fast allen Projekten, an denen ich arbeite oder gearbeitet habe.

  • Die Erwartung steht sehr weit oben im Test und nicht am Ende.
  • Sollten die Parameter nicht stimmen kommt es zu unerwarteten NullPointerException.
  • Der Test ist implizit und Tests müssen immer explizit sein um verständlich und wartbar zu sein.

Beispiel für einen expliziten Test

Teilen Sie diesen Beitrag

Das könnte dich auch interessieren …