Behavior-Driven Development (BDD) ist ein Ansatz in der Softwareentwicklung, der den Fokus auf das Verhalten eines Systems setzt. Dies wird durch eine entsprechende Strukturierung von Testfällen unterstützt. Häufig kommt dabei der Dreiklang aus Given-When-Then zum Einsatz. Wie mein Kollege Tobias Göschel gezeigt hat, ließ sich diese Struktur bereits mit JUnit 4 verwenden . In diesem Blopost werde ich zeigen, wie BDD mithilfe der @Nested
-Annotation in JUnit 5 umgesetzt werden kann. Wer noch nicht mit @Nested
gearbeitet hat, dem sei mein Einführungsblogpost zu diesem Thema ans Herz gelegt.
Vorüberlegung
Als Beispiel für diesen Blogpost möchte ich die Stack -Klasse aus der Java-Standardbibliothek verwenden. Stellen wir uns den einfachsten Test für diese Klasse vor: Wenn ein Stack leer ist, dann sollte seine Größe 0 sein. Mit JUnit könnte man das so testen:
1@Test
2void emptyStackShouldHaveSizeZero() {
3 // given
4 stack.clear();
5
6 // when
7 int size = stack.size();
8
9 // then
10 assertEquals(0, size);
11}
In klassischen, „flachen“ JUnit-Tests findet man häuft diese implizite Given-When-Then-Struktur. Besonders in objektorientierten Systemen ergibt sich das ganz natürlich aus der Tatsache, dass die zu testenden Objekte zunächst in den richtigen Zustand versetzt werden müssen (Given), danach wird eine Aktion ausgeführt (When) und schließlich wird das Ergebnis dieser Aktion überprüft (Then).
Wenn wir mehrere Tests mit einem leeren Stack machen wollen, müssen wir jeweils das Given in den Testmethoden wiederholen. Alternativ können wir das Herstellen eines leeren Stacks in eine Setup-Methode auslagern. Dieser Ansatz funktioniert allerdings nur so lange, bis wir den ersten Test mit einem gefüllten Stack schreiben wollen. Von da an müssen wir für alle Tests mit gefülltem Stack zunächst ein Element hinzufügen. Diese Wiederholung können wir wiederum beheben, indem wir eine weitere Stack-Instanz anlegen und in der Setup-Methode befüllen.
Für den Test eines Stacks mag dies noch gut handhabbar sein. In Geschäftsanwendungen arbeiten wir aber in der Regel mit deutlich komplexeren Objekten und müssen deren Interaktionen mit anderen Objekten beachten. Hier sieht man dann häufig riesige Setup-Blöcke, in denen mehrere Objektinstanzen und Mocks erzeugt und konfiguriert werden, die dann jeweils nur von einigen Testmethoden verwendet werden. Möchte man einen Test hinzufügen, führen schon minimale Änderungen der Konfiguration dazu, dass andere Tests fehlschlagen. Die Folge: Test Setups werden kopiert, die Tests werden unübersichtlicher, die Wartbarkeit leidet.
Bessere Test durch geschachtelte Kontexte
Hier zeigt sich ein zentrales Problem einer flachen Teststruktur: Obwohl unterschiedliche Setups aufeinander aufbauen, lässt sich dieser Fakt nicht in der Testimplementierung abbilden. Im Beispiel des Stacks können wir uns den gefüllten Stack als Ableitung des leeren Stacks vorstellen. Wir bekommen einen gefüllten Stack, indem wir dem leeren Stack mindestens ein Element hinzufügen. Mit JUnit 5 können wir genau das durch geschachtelte Testklassen erreichen:
1class StackTests {
2
3 private Stack<String> stack;
4
5 @BeforeEach
6 void setUp() {
7 stack = new Stack<>();
8 }
9
10 @Nested
11 class GivenAnEmptyStack {
12
13 @BeforeEach
14 void setUp() {
15 stack.clear();
16 }
17
18 @Test
19 void thenTheSizeOfTheStackShouldBeZero() {
20 assertEquals(0, stack.size());
21 }
22
23 // more tests for verifying empty stack behavior
24
25 @Nested
26 class WhenAnElementIsAdded {
27
28 @BeforeEach
29 void setUp() {
30 stack.add("elem");
31 }
32
33 @Test
34 void thenTheSizeOfTheStackShouldBeOne() {
35 assertEquals(1, stack.size());
36 }
37
38 // more test for verifying filled stack behavior
39 }
40 }
41}
Wie zu sehen ist, benötigen wir nur eine Stack-Instanz im Test, können diese aber für die verschiedenen Fälle unterschiedlich konfigurieren. Dies gelingt dadurch, dass jede geschachtelte Testklasse einen eigenen Subkontext aufbaut: Das Hinzufügen des Elements "elem"
ist nur für die Testmethoden innerhalb der Klasse WhenAnElementIsAdded
sichtbar. Diese Struktur führt auch dazu, dass die Testmethoden jeweils nur noch die Assertion enthalten – es ist offensichtlicher, was hier eigentlich geprüft wird. Der Aufbau des benötigten Zustands verschiebt sich in die Setup-Methoden und kann so zwischen den Testmethoden geteilt werden.
Darüber hinaus wird diese Struktur von IDEs wie IntelliJ sehr übersichtlich dargestellt:
Zu Verbesserung der Lesbarkeit habe ich hier noch mit mit @DisplayName
gearbeitet. Das vollständige Codebeispiel findet ihr auf GitHub .
Fazit
Klassische JUnit-Tests mit „flacher“ Teststruktur werden häufig durch sich wiederholende Setups unübersichtlich. In diesem Blogpost habe ich gezeigt, wie sich dies durch den Given-When-Then-Ansatz vermeiden lässt. Dazu habe ich die @Nested
-Annotation aus JUnit 5 verwendet, um die unterschiedlichen Testkontexte ineinander zu schachteln und so die Wiederverwendung zu erhöhen.
Weitere Beiträge
von Benedikt Ritter
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Benedikt Ritter
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.