Einleitung: Warum Tests ohne Mocks wie Kaffee ohne Koffein sind?
\n\nHallo, Kollege! Wenn du in Kotlin programmierst und möchtest, dass dein Code nicht bei jedem zweiten Commit zerbricht, wirst du früher oder später auf Tests stoßen. Und in Tests auf Mock-Objekte und Fixtures. Ohne sie werden aus Unit-Tests Integrationstests: Du testest nicht eine einzelne Klasse, sondern eine ganze Kette von Abhängigkeiten. Fällt die Datenbank aus oder gibt die API einen 500er zurück, fällt der Test – obwohl dein Code perfekt sein könnte.
\n\nIn diesem Artikel schauen wir uns an, wie du Mock-Objekte und Fixtures in Kotlin-Tests richtig einsetzt, damit deine Tests schnell, zuverlässig und aussagekräftig sind. Los geht's!
\n\nWas ist ein Mock und warum brauchst du ihn in Kotlin?
\n\nEin Mock-Objekt ist eine Nachbildung eines echten Objekts. Anstatt einen ganzen Server oder eine Datenbank hochzufahren, erstellst du einen „Platzhalter", der sich so verhält, wie du es vorgibst. So isolierst du den zu testenden Code von der Außenwelt.
\n\nGrundlegende Begriffe
\n- \n
- Mock – Ein Objekt, dessen Methodenverhalten wir kontrollieren. Wir legen fest, was es zurückgibt, und prüfen, ob es aufgerufen wurde. \n
- Stub – Ein Spezialfall eines Mocks, der nur vordefinierte Werte zurückgibt. Er prüft keine Aufrufe. \n
- Fixture – Ein Satz vorbereiteter Daten oder Zustände, die für die Testausführung nötig sind. Zum Beispiel ein Testbenutzer oder Datenbankeinstellungen. \n
In der Kotlin-Welt gibt es zwei Hauptakteure: MockK (nativ für Kotlin) und Mockito (beliebt in Java). Wir verwenden MockK – es harmoniert besser mit Koroutinen, Erweiterungsfunktionen und sealed-Klassen.
\n\nProjekt-Setup: MockK und JUnit 5 hinzufügen
\n\nZunächst fügen wir die Abhängigkeiten in build.gradle.kts hinzu:
dependencies {\n testImplementation("io.mockk:mockk:1.13.8")\n testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")\n}\n\n\nJetzt können wir Tests schreiben. Erstellen wir einen einfachen Service und testen ihn mit Mocks.
\n\nErstes Beispiel: Ein Repository mocken
\n\nStell dir vor, wir haben einen UserService, der von einem UserRepository abhängt:
class UserService(private val repository: UserRepository) {\n fun getUserName(id: Long): String {\n val user = repository.findById(id)\n ?: throw IllegalArgumentException("User not found")\n return user.name\n }\n}\n\n\nJetzt schreiben wir einen Test mit einem Mock-Objekt für das Repository:
\n\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.verify\nimport org.junit.jupiter.api.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFailsWith\n\nclass UserServiceTest {\n\n private val repository = mockk<UserRepository>()\n private val service = UserService(repository)\n\n @Test\n fun `should return user name when user exists`() {\n // Arrange\n every { repository.findById(1L) } returns User(1L, "Alice")\n\n // Act\n val name = service.getUserName(1L)\n\n // Assert\n assertEquals("Alice", name)\n verify { repository.findById(1L) }\n }\n\n @Test\n fun `should throw exception when user not found`() {\n // Arrange\n every { repository.findById(any()) } returns null\n\n // Act & Assert\n assertFailsWith<IllegalArgumentException> {\n service.getUserName(99L)\n }\n }\n}\n\n\nBeachte: every legt das Verhalten des Mocks fest, verify prüft, ob die Methode aufgerufen wurde. Das ist die Grundlage von Mock-Objekten und Fixtures in Kotlin-Tests.
Fixtures: Daten im Voraus vorbereiten
\n\nWenn du viele Tests hast, ist es schlechte Praxis, die Datenerstellung zu duplizieren. Hier kommen Fixtures ins Spiel. In Kotlin lassen sie sich bequem