Über unsMediaKontaktImpressum
Andreas Jürgensen 13. Februar 2026

Clean-Test-Code - mit JUnit und AssertJ

Der Begriff "Clean Code" ist spätestens seit dem gleichnamigen Buch von Robert C. Martin aus dem Jahr 2008 in aller Munde. Doch wie auch im Buch meint Clean Code in der Regel nur den Produktivcode. Gleichzeitig wurden automatisierte Tests durch Bewegungen wie TDD und CI/CD immer verbreiteter und wichtiger. Moderne Softwareprojekte sind ohne eine hohe Testabdeckung nicht mehr denkbar. Daraus folgt, dass ein großer Teil oder sogar der Großteil des Codes Test-Code ist. Dieser Artikel versucht, Clean-Code-Prinzipien wie Wartbarkeit, Lesbarkeit und Erweiterbarkeit auf den Test-Code anzuwenden. Die Beispiele in diesem Artikel sind in Java geschrieben und verwenden daher JUnit und AssertJ, die zugrundeliegenden Konzepte sind aber auch auf andere Programmiersprachen. Test Frameworks und Assertion Libraries übertragbar.

Qualitätsanforderungen an Test-Code

Nehmen wir an, der erste Schritt ist getan und es wird akzeptiert, dass die Qualität des Test-Codes wichtig ist. Können die Qualitätsanforderungen an Produktiv-Code eins zu eins übernommen werden? Nein, leider nicht. Test-Code erfüllt eine andere Aufgabe als Produktiv-Code, weshalb auch die Anforderungen variieren. Daher definieren wir zunächst die Qualitätsanforderungen an Test-Code und bauen später darauf auf.

Ein grundlegendes Qualitätsmerkmal ist die Verständlichkeit des Test-Codes. Im Gegensatz zum Produktiv-Code, der ausdrücken soll, was der Code tut, soll der Test-Code ausdrücken, welche Anforderungen die Software erfüllt. Je nach Test-Stufe ist eine unterschiedliche Abstraktionsebene angemessen. Während innenliegende Tests einer einzelnen Methode diese durchaus direkt verwenden können, sollten ganz außen ansetzende Akzeptanztests so weit vom Code abstrahieren, dass sie auch von Fach-Expertinnen und -experten verstanden werden können.

Neben der Verständlichkeit des Tests ist die Wartbarkeit ein weiteres wichtiges Qualitätsmerkmal. Wie auch im Produktiv-Code zeigt sich Wartbarkeit auf verschiedene Weisen, meint aber grundsätzlich, wie einfach Änderungen vorgenommen werden können. Klassischerweise bedeutet Wartbarkeit, wie leicht der Test-Code an sich ändernde Anforderungen angepasst werden kann. Das ist genau so wie beim Produktiv-Code. In Bezug auf Test-Code kommt noch ein weiterer Aspekt hinzu, und zwar, wenn der Produktiv-Code refaktorisiert wird. Refactoring bedeutet, dass zwar der Code verändert wird, die Software aber noch die gleichen Anforderungen erfüllt. Da die Anforderungen in Form von Test-Code implementiert werden, sollte wartbarer Test-Code nicht geändert werden müssen, wenn der Produktiv-Code refaktorisiert wird.

Das letzte betrachtete Qualitätsmerkmal ist die Erweiterbarkeit. Verständlichkeit und Wartbarkeit fördern auch eine bessere Erweiterbarkeit, aber Erweiterbarkeit bringt noch eine neue, eigene Dimension mit sich. Für Produktiv-Code ist das Open-Closed-Prinzip (OCP) das bekannteste Prinzip zur Förderung von Erweiterbarkeit. Das OCP besagt, dass Softwarekomponenten offen für Erweiterung und geschlossen für Veränderung sein sollten. Wenn das OCP im Produktiv-Code umgesetzt wird, führt das auch indirekt zu besserer Erweiterbarkeit im Test-Code. Aber das OCP kann auch explizit auf Test-Code angewendet werden.

Das System unter Test benennen

Die Software-Komponente, die getestet wird, wird üblicherweise als System unter Test (engl. system under test oder subject under test) bezeichnet. Auch die Abkürzung SUT ist weit verbreitet. Eine kleine Angewohnheit mit großer Wirkung besteht darin, das Einstiegsobjekt der zu testenden Komponente durch eine Variable mit dem Namen “sut” zu referenzieren. Durch diese explizite Benennung des SUT ist sofort ersichtlich, was getestet wird und was nur drumherum benötigt wird. Bei der Variable kann es sich um eine lokale Variable innerhalb der Testmethode oder um eine Instanzvariable der Testklasse handeln. Wenn eine Instanzvariable verwendet wird, sollte das SUT vor jedem Test neu erzeugt werden, um Abhängigkeiten zwischen Tests zu vermeiden.

class UserServiceTest {
	@Test
	void test() {
		...
		UserService sut = new UserService();
		...
	}
}
Variante 2: Instanzvariable
class UserServiceTest {
	UserService sut;
	
	@BeforeEach
	void setUp() { sut = new UserService(); }

	@Test
	void test() {...}
}

Given-When-Then oder Arrange-Act-Assert

Eine gute Strukturierung innerhalb einer Testmethode ist die Grundlage für sauberen Test-Code. Given-When-Then und Arrange-Act-Assert sind zwei Namen für das gleiche Muster: eine Dreiteilung der Testmethode. Der erste Teil ist der Given- bzw. Arrange-Teil. In diesem Teil der Testmethode findet der Testaufbau statt. Dabei werden benötigte Datenstrukturen aufgebaut und Test Doubles für Dependencies konfiguriert. Außerdem wird das SUT in den Ausgangszustand für den Test versetzt. Im When- bzw. Act-Teil wird der Test durchgeführt. In der Regel besteht die Testdurchführung aus einem einzigen Methodenaufruf und ggf. dem Abspeichern des Rückgabewerts für anschließende Überprüfungen. Wenn Fehlerfälle getestet werden, gehört auch das Fangen und Abspeichern von geworfenen Exceptions zum Given-/Act-Teil. Im dritten und letzten Teil, dem Then- bzw. Assert-Teil, wird das Ergebnis der Testdurchführung überprüft. Dafür können JUnit-eigene Assertions oder gesonderte Assertion Libraries wie AssertJ benutzt werden. Die drei Teile können durch Leerzeilen getrennt werden, um die Struktur deutlich zu machen. Diese Trenn-Leerzeilen sollten die einzigen Leerzeilen in einer Testmethode sein. Auch Inline-Kommentare zur Kenntlichmachung der einzelnen Teile sind weit verbreitet und haben den Vorteil, dass die Struktur auch für andere erkennbar wird.

Statt so:

 @Test
void testUserBalanceIsUpdatedAfterDeposit() {
	User user = new User(“Alice”);
	user.deposit(100);
	assertThat(user.balance()).isEqualTo(100);
}

lieber so: 

@Test
void testUserBalanceIsUpdatedAfterDeposit() {
	// Given
	User user = new User(“Alice”);

	// When
	user.deposit(100);

	// Then
	assertThat(user.balance()).isEqualTo(100);
}

oder so: 

@Test
void testUserBalanceIsUpdatedAfterDeposit() {
	// Arrange
	User user = new User(“Alice”);

	// Act
	user.deposit(100);

	// Assert
	assertThat(user.balance()).isEqualTo(100);
}

Fachliche Beschreibung

Testmethoden sollten fachlich beschrieben sein, damit klar erkennbar wird, welche Anforderung durch den Test überprüft wird. Eine gute fachliche Benennung erhöht die Lesbarkeit und Verständlichkeit von Tests, auch für Personen, die nicht unmittelbar am Code mitgewirkt haben. Statt technische Details oder Implementierungsaspekte zu betonen, hilft eine fachliche Beschreibung dabei, die Intention des Tests zu verdeutlichen und Missverständnisse zu vermeiden. So können Tests als lebendige Spezifikation dienen.

Statt so:

@Test
void loginWithValidCredentialsSetsAuthenticatedFlag() {...}

lieber so: 

@Test
void userCanLoginWithValidCredentials() {...}

Darüber hinaus ist es wichtig, dass auch Methoden für den Testaufbau, die Testdurchführung und die Testauswertung fachlich benannt werden. Wenn solche Methoden rein technisch oder implementierungsnah beschrieben sind, entsteht eine enge Kopplung an die aktuelle Struktur des Produktionscodes. Fachlich benannte Methoden dagegen abstrahieren technische Details und beschreiben die Absicht des Testschrittes. Dadurch bleiben Tests stabil gegenüber Refactorings im Code: Selbst wenn sich interne Implementierungen ändern, bleibt die fachliche Sprache in den Tests unverändert.

Custom Assertions

Custom Assertions in AssertJ sind ein wirksames Mittel, um Tests fachlich aussagekräftiger und stabiler zu gestalten. Statt in jedem Test einzelne technische Eigenschaften eines Objekts mit mehreren Standard-Assertions zu überprüfen, lassen sich eigene Assertions definieren, die diese Prüfungen in einer fachlichen Sprache bündeln. So kann etwa anstelle von

assertThat(order.getStatus()).isEqualTo(Status.CONFIRMED);
assertThat(order.getConfirmationDate()).isNotNull();

eine Custom Assertion genutzt werden wie

assertThat(order).isConfirmed();

Die Implementierung solcher Methoden erfolgt durch das Erweitern von AbstractAssert von AssertJ. Dort lassen sich Methoden wie isConfirmed() hinterlegen, die intern die notwendigen technischen Checks ausführen.

Der Vorteil liegt darin, dass die Tests nicht mehr an technische Details gebunden sind, sondern in einer domänenspezifischen Sprache das Verhalten beschreiben. Das erhöht die Lesbarkeit, erleichtert die Verständlichkeit für Fachfremde und macht die Tests stabiler gegenüber Refactorings: Wenn sich die interne Implementierung ändert, wird lediglich die Custom Assertion angepasst, während die Tests unverändert in ihrer fachlichen Ausdruckskraft bestehen bleiben.

Aufteilung von Tests

Beim Schneiden von Tests sollte im Mittelpunkt stehen, dass jeder Test ein klar abgegrenztes Verhalten überprüft. Eine hilfreiche Leitlinie dafür ist “Nur ein Exit Point pro Test”. Ein Exit Point ist die von außen sichtbare Reaktion auf einen Input, etwa ein Return-Wert, ein veränderter Zustand oder ein geworfener Fehler. So bleibt der Test fokussiert und kann nur aus einem Grund fehlschlagen. Eng verwandt ist die Regel “Nur ein Assert pro Test“: Sie sorgt dafür, dass genau dieser eine Exit Point geprüft wird, ohne dass sich mehrere, potenziell unabhängige Überprüfungen im selben Test vermischen. Praktisch bedeutet das, dass Sub Assertions erlaubt sein können, solange sie gemeinsam denselben Exit Point validieren. In der Kombination sorgen beide Regeln dafür, dass Tests präzise, gut verständlich und langfristig wartbar bleiben.

Trennung von Infrastruktur und Fachlichkeit

Im Test-Code ist es sinnvoll, eine klare Trennung zwischen fachlichem und technischem Code vorzunehmen. Fachlicher Code beschreibt das Was – also das erwartete Verhalten und die fachliche Intention. Dieser Teil gehört direkt in die Testmethode, damit sofort ersichtlich ist, welches Verhalten geprüft wird. Technischer Code dagegen beschreibt das Wie – also Aufbau, Konfiguration und technische Infrastruktur der Tests. Dazu zählt beispielsweise das Starten und Schließen von Datenbankverbindungen.

Dieser technische Code sollte möglichst nicht in den Testmethoden selbst stehen, sondern in die Lifecycle-Methoden von JUnit wie @BeforeEach oder @AfterEach ausgelagert werden. Für wiederkehrende oder komplexere Infrastrukturaufgaben bietet es sich an, eigene JUnit5 Extensions zu schreiben, die technisches Setup und Teardown automatisiert übernehmen. Auf diese Weise bleiben die Testmethoden frei von technischen Details, klar lesbar und konzentrieren sich ausschließlich auf die fachliche Aussage des Tests.

Contract-Tests für Interfaces in Test-Interfaces

JUnit5 bietet die Möglichkeit, Tests in Interfaces zu definieren. Diese Interfaces können anschließend von konkreten Testklassen implementiert werden. Dieses Feature eignet sich besonders gut für Contract-Tests [1]. Ein Produktiv-Code-Interface beschreibt die zu implementierende API und ein zugehöriges Test-Interface enthält die dazugehörigen Tests. Die Test-Klassen für Implementierungen der API implementieren dann das Test-Interface. Dadurch ergeben sich klare Vorteile für die Erweiterbarkeit: Contract-Tests müssen nicht für jede Implementierung neu geschrieben werden, sondern liegen zentral im Interface und werden automatisch auf alle Implementierungen angewendet. Sobald die Spezifikation eines Interfaces erweitert wird, können die entsprechenden Tests direkt im Test-Interface ergänzt werden und stehen sofort allen Implementierungen zur Verfügung. Neue Klassen, die ein solches Interface implementieren, profitieren ebenfalls, da sie ohne zusätzlichen Aufwand gegen die bestehenden Verträge geprüft werden können. Auf diese Weise bleiben die Tests konsistent und der Produktiv-Code leicht erweiterbar.

Quellen

[1] JUnit User Guide

Autor
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben