Mocking im Cloud-Native-Zeitalter: Ein Leitfaden für Entwickler, die noch Zeit zum Leben haben wollen
"Der beste Mock ist der, den du nie schreiben musstest." — Jeder Entwickler, der schon mal um 3 Uhr morgens Testcontainers debuggt hat
Einleitung: Warum wir überhaupt mocken
Willkommen in der wunderbaren Welt des Cloud-Native-Testings, wo Microservices sich wie Kaninchen vermehren und jeder Service mindestens drei Datenbanken, zwei Message-Broker und eine existenzielle Krise hat.
Die gute Nachricht: Es gibt mittlerweile mehr Mock-Frameworks als Gründe, nicht zu testen. Die schlechte Nachricht: Du musst dich entscheiden.
Dieser Leitfaden basiert auf dem aktuellen Stand der Technik (November 2025) und berücksichtigt Java 25 (das aktuelle LTS-Release, veröffentlicht im September 2025) sowie Kotlin in allen seinen Facetten.
1. Die Klassiker: Mockito und MockK
1.1 Mockito – Der Java-Veteran
Mockito ist wie ein alter Freund: zuverlässig, manchmal etwas umständlich, aber immer da, wenn man ihn braucht.
// Java 25 mit Mockito
@ExtendWith(MockitoExtension.class)
class KundenServiceTest {
@Mock
private KundenRepository repository;
@InjectMocks
private KundenService service;
@Test
void sollteKundenFinden() {
// given
when(repository.findById(42L))
.thenReturn(Optional.of(new Kunde("Max", "Mustermann")));
// when
var kunde = service.getKunde(42L);
// then
assertThat(kunde.vorname()).isEqualTo("Max");
verify(repository).findById(42L);
}
}
Aktuelle Version: Mockito 5.x mit vollständiger Unterstützung für Java 25
Wichtige Abhängigkeiten:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.0</version>
<scope>test</scope>
</dependency>
<!-- Für finale Klassen (Standard in Kotlin!) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.14.0</version>
<scope>test</scope>
</dependency>
1.2 MockK – Der Kotlin-Native Ansatz
MockK wurde von Grund auf für Kotlin entwickelt und versteht dessen Eigenheiten wie finale Klassen, Extension Functions und Coroutines.
// Kotlin mit MockK
class BestellServiceTest {
private val lagerService = mockk<LagerService>()
private val bestellService = BestellService(lagerService)
@Test
fun `sollte Bestellung verarbeiten wenn Lager ausreichend`() {
// given
every { lagerService.pruefeVerfuegbarkeit(any()) } returns true
coEvery { lagerService.reserviere(any()) } returns ReservierungsId("ABC123")
// when
val ergebnis = runTest {
bestellService.bestellungAufgeben(Bestellung(artikelId = 42, menge = 5))
}
// then
ergebnis shouldBe BestellStatus.BESTAETIGT
coVerify { lagerService.reserviere(match { it.artikelId == 42 }) }
}
}
Kernfeatures von MockK:
| Feature | Mockito | MockK |
|---|---|---|
| Finale Klassen mocken | Erfordert mockito-inline | ✅ Nativ |
| Extension Functions | ❌ Nicht möglich | ✅ Mit mockkStatic |
| Coroutines | Umständlich | ✅ coEvery, coVerify |
| Kotlin Objects (Singletons) | ❌ | ✅ mockkObject |
| DSL-Syntax | ❌ | ✅ Kotlin-idiomatic |
Coroutines mocken:
@Test
fun `sollte asynchrone Operationen mocken`() = runTest {
val api = mockk<ExterneApi>()
coEvery { api.holeDaten() } returns Daten("test")
coEvery { api.speichere(any()) } just Runs
val service = DatenService(api)
service.verarbeite()
coVerify(exactly = 1) { api.holeDaten() }
}
2. Spring Boot Testing
2.1 @MockBean und @SpyBean
Spring Boot macht das Mocken von Beans zum Kinderspiel – oder zumindest zu einem etwas größeren Kind.
@SpringBootTest
class BestellControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ZahlungsService zahlungsService;
@SpyBean
private AuditService auditService;
@Test
void sollteBestellungAnlegen() throws Exception {
// given
when(zahlungsService.autorisiereZahlung(any()))
.thenReturn(new Autorisierung("AUTH-123", Status.ERFOLGREICH));
// when & then
mockMvc.perform(post("/api/bestellungen")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"artikelId": 42,
"menge": 2,
"zahlungsmethode": "KREDITKARTE"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value("BESTAETIGT"));
verify(auditService).protokolliere(any(BestellEvent.class));
}
}
2.2 @WebMvcTest für Controller-Tests
@WebMvcTest(ProduktController.class)
class ProduktControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProduktService produktService;
@Test
void sollteProduktZurueckgeben() throws Exception {
when(produktService.findById(1L))
.thenReturn(Optional.of(new Produkt(1L, "Laptop", 999.99)));
mockMvc.perform(get("/api/produkte/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Laptop"));
}
}
2.3 WebTestClient für Reactive Stacks
@WebFluxTest(ReaktivController::class)
class ReaktivControllerTest {
@Autowired
private lateinit var webTestClient: WebTestClient
@MockBean
private lateinit var reaktivService: ReaktivService
@Test
fun `sollte Flux von Elementen streamen`() {
// given
every { reaktivService.alleElemente() } returns
Flux.just(Element("A"), Element("B"), Element("C"))
// when & then
webTestClient.get()
.uri("/api/elemente")
.exchange()
.expectStatus().isOk
.expectBodyList(Element::class.java)
.hasSize(3)
}
}
3. Quarkus Testing
Quarkus hat einen erfrischend anderen Ansatz: Dev Services starten automatisch Container für dich, ohne dass du auch nur eine Zeile Konfiguration schreiben musst. Magie? Nein, nur sehr viel cleverer Code.
3.1 @QuarkusTest mit @InjectMock
@QuarkusTest
class GrussServiceTest {
@InjectMock
GrussRepository repository;
@Inject
GrussService service;
@Test
void sollteGrussErzeugen() {
// given
Mockito.when(repository.holeVorlage("morgen"))
.thenReturn("Guten Morgen, {name}!");
// when
String gruss = service.erzeuge("morgen", "Welt");
// then
assertThat(gruss).isEqualTo("Guten Morgen, Welt!");
}
}
3.2 Dev Services – Der automatische Container-Starter
Quarkus erkennt automatisch, welche Infrastruktur du brauchst und startet Container:
# application.properties - mehr brauchst du nicht!
quarkus.datasource.db-kind=postgresql
# Kein URL, kein Username, kein Password - Dev Services regelt das
@QuarkusTest
class KundenRepositoryTest {
@Inject
KundenRepository repository;
@Test
@Transactional
void sollteKundenSpeichern() {
// PostgreSQL-Container läuft automatisch!
var kunde = new Kunde("Test", "test@example.com");
repository.persist(kunde);
assertThat(kunde.id).isNotNull();
}
}
3.3 QuarkusTestResourceLifecycleManager für Custom Container
class ConsulTestResource : QuarkusTestResourceLifecycleManager {
private lateinit var consul: ConsulContainer
override fun start(): Map<String, String> {
consul = ConsulContainer("consul:1.15")
consul.start()
return mapOf(
"quarkus.consul-config.agent.host-port" to consul.hostPort
)
}
override fun stop() {
consul.stop()
}
}
@QuarkusTest
@QuarkusTestResource(ConsulTestResource::class)
class ConsulIntegrationTest {
// Tests hier
}
4. Testcontainers – Container für alle
Testcontainers ist der Schweizer Taschenmesser des Integrationstests. Läuft ein Container auf Docker? Dann kannst du ihn testen.
4.1 Datenbank-Container
@Testcontainers
@SpringBootTest
class DatenbankIntegrationTest {
companion object {
@Container
@JvmStatic
val postgres = PostgreSQLContainer("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
@Container
@JvmStatic
val mongo = MongoDBContainer("mongo:7.0")
@DynamicPropertySource
@JvmStatic
fun properties(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url") { postgres.jdbcUrl }
registry.add("spring.datasource.username") { postgres.username }
registry.add("spring.datasource.password") { postgres.password }
registry.add("spring.data.mongodb.uri") { mongo.replicaSetUrl }
}
}
@Autowired
lateinit var kundenRepository: KundenRepository
@Test
fun `sollte Kunde in PostgreSQL speichern`() {
val kunde = Kunde(name = "Integration Test")
val gespeichert = kundenRepository.save(kunde)
assertThat(gespeichert.id).isNotNull()
}
}
4.2 Container wiederverwenden (Testgeschwindigkeit!)
# ~/.testcontainers.properties
testcontainers.reuse.enable=true
companion object {
@Container
@JvmStatic
val postgres = PostgreSQLContainer("postgres:16-alpine")
.withReuse(true) // Container überlebt Testläufe!
}
4.3 NoSQL-Datenbanken
MongoDB:
@Testcontainers
class MongoRepositoryTest {
@Container
static MongoDBContainer mongo = new MongoDBContainer("mongo:7.0");
private MongoClient client;
@BeforeEach
void setup() {
client = MongoClients.create(mongo.getReplicaSetUrl());
}
@Test
void sollteDocumentSpeichern() {
var db = client.getDatabase("test");
var collection = db.getCollection("users");
collection.insertOne(new Document("name", "Max"));
assertThat(collection.countDocuments()).isEqualTo(1);
}
}
Redis:
@Testcontainers
class CacheServiceTest {
companion object {
@Container
@JvmStatic
val redis = GenericContainer("redis:7-alpine")
.withExposedPorts(6379)
}
@Test
fun `sollte Wert cachen`() {
val jedis = Jedis(redis.host, redis.firstMappedPort)
jedis.set("key", "value")
assertThat(jedis.get("key")).isEqualTo("value")
}
}
Elasticsearch:
@Testcontainers
class SucheServiceTest {
@Container
static ElasticsearchContainer elastic =
new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.11.0")
.withEnv("xpack.security.enabled", "false");
@Test
void sollteDocumenteIndizieren() {
var client = RestClient.builder(
HttpHost.create(elastic.getHttpHostAddress())
).build();
// Index erstellen, Dokumente speichern, suchen...
}
}
5. Kafka Testing
5.1 EmbeddedKafka mit Spring
@SpringBootTest
@EmbeddedKafka(
partitions = 1,
topics = ["bestellungen", "benachrichtigungen"],
brokerProperties = ["listeners=PLAINTEXT://localhost:9092"]
)
class KafkaIntegrationTest {
@Autowired
lateinit var kafkaTemplate: KafkaTemplate<String, BestellungEvent>
@SpyBean
lateinit var bestellungListener: BestellungListener
@Test
fun `sollte Bestellung verarbeiten`() {
// given
val event = BestellungEvent(
bestellungId = "123",
artikelIds = listOf(1, 2, 3)
)
// when
kafkaTemplate.send("bestellungen", event).get()
// then
verify(bestellungListener, timeout(5000))
.onBestellung(argThat { bestellungId == "123" })
}
}
5.2 Kafka mit Testcontainers
@Testcontainers
class KafkaTestcontainersTest {
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
);
@Test
void sollteNachrichtProducerenUndKonsumieren() throws Exception {
var props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
try (var producer = new KafkaProducer<String, String>(props)) {
producer.send(new ProducerRecord<>("test-topic", "key", "Hallo Kafka!")).get();
}
var consumerProps = new Properties();
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group");
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
try (var consumer = new KafkaConsumer<String, String>(consumerProps)) {
consumer.subscribe(List.of("test-topic"));
var records = consumer.poll(Duration.ofSeconds(10));
assertThat(records).hasSize(1);
assertThat(records.iterator().next().value()).isEqualTo("Hallo Kafka!");
}
}
}
5.3 Schema Registry mocken
@Testcontainers
class AvroKafkaTest {
companion object {
private val network = Network.newNetwork()
@Container
@JvmStatic
val kafka = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"))
.withNetwork(network)
@Container
@JvmStatic
val schemaRegistry = GenericContainer("confluentinc/cp-schema-registry:7.5.0")
.withNetwork(network)
.withExposedPorts(8081)
.withEnv("SCHEMA_REGISTRY_HOST_NAME", "schema-registry")
.withEnv("SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS",
"PLAINTEXT://${kafka.networkAliases[0]}:9092")
.dependsOn(kafka)
}
}
6. HTTP-Mocking mit WireMock
WireMock simuliert externe HTTP-APIs, damit dein Test nicht von der Verfügbarkeit des Zahlungsdienstleisters um 3 Uhr morgens abhängt.
6.1 WireMock Basics
@WireMockTest(httpPort = 8089)
class ExterneApiClientTest {
private val client = ExterneApiClient("http://localhost:8089")
@Test
fun `sollte erfolgreiche Antwort verarbeiten`(wireMock: WireMockRuntimeInfo) {
// given
stubFor(get(urlEqualTo("/api/users/42"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"id": 42,
"name": "Max Mustermann",
"email": "max@example.com"
}
""")))
// when
val user = client.getUser(42)
// then
assertThat(user.name).isEqualTo("Max Mustermann")
verify(getRequestedFor(urlEqualTo("/api/users/42"))
.withHeader("Accept", containing("application/json")))
}
@Test
fun `sollte Timeout graceful behandeln`() {
// given
stubFor(get(anyUrl())
.willReturn(aResponse()
.withStatus(200)
.withFixedDelay(5000))) // 5 Sekunden Verzögerung
// when & then
assertThrows<TimeoutException> {
client.getUser(42)
}
}
@Test
fun `sollte Retry bei 503 durchführen`() {
// given
stubFor(get(urlEqualTo("/api/users/42"))
.inScenario("Retry")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(aResponse().withStatus(503))
.willSetStateTo("Erster Versuch fehlgeschlagen"))
stubFor(get(urlEqualTo("/api/users/42"))
.inScenario("Retry")
.whenScenarioStateIs("Erster Versuch fehlgeschlagen")
.willReturn(aResponse()
.withStatus(200)
.withBody("""{"id": 42, "name": "Max"}""")))
// when
val user = client.getUserWithRetry(42)
// then
assertThat(user.name).isEqualTo("Max")
verify(2, getRequestedFor(urlEqualTo("/api/users/42")))
}
}
6.2 WireMock mit Spring Boot
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class ZahlungsServiceIntegrationTest {
@Autowired
lateinit var zahlungsService: ZahlungsService
@Value("\${wiremock.server.port}")
var wireMockPort: Int = 0
@Test
fun `sollte Zahlung autorisieren`() {
stubFor(post(urlEqualTo("/v1/payments/authorize"))
.withRequestBody(matchingJsonPath("$.amount", equalTo("99.99")))
.willReturn(aResponse()
.withStatus(200)
.withBody("""
{
"transactionId": "TXN-123",
"status": "AUTHORIZED"
}
""")))
val result = zahlungsService.autorisiere(
Zahlungsanfrage(betrag = 99.99, waehrung = "EUR")
)
assertThat(result.status).isEqualTo(ZahlungsStatus.AUTORISIERT)
}
}
7. Contract Testing mit Pact
7.1 Warum Contract Testing?
Stell dir vor, Team A ändert eine API-Antwort von "userId" zu "user_id". Integration Tests laufen grün (natürlich mocken sie alles). Produktion explodiert um 3 Uhr morgens.
Contract Testing verhindert genau das.
7.2 Consumer-Side (Der API-Nutzer)
@ExtendWith(PactConsumerTestExt::class)
@PactTestFor(providerName = "BenutzerService", port = "8080")
class BenutzerClientPactTest {
@Pact(consumer = "BestellService")
fun pactFuerBenutzerAbfrage(builder: PactDslWithProvider): RequestResponsePact {
return builder
.given("Benutzer 42 existiert")
.uponReceiving("Anfrage für Benutzer 42")
.path("/api/benutzer/42")
.method("GET")
.willRespondWith()
.status(200)
.headers(mapOf("Content-Type" to "application/json"))
.body(PactDslJsonBody()
.integerType("id", 42)
.stringType("name", "Max Mustermann")
.stringType("email", "max@example.com"))
.toPact()
}
@Test
@PactTestFor(pactMethod = "pactFuerBenutzerAbfrage")
fun `sollte Benutzer vom Provider laden`() {
val client = BenutzerClient("http://localhost:8080")
val benutzer = client.getBenutzer(42)
assertThat(benutzer.name).isEqualTo("Max Mustermann")
}
}
7.3 Provider-Side (Der API-Anbieter)
@Provider("BenutzerService")
@PactBroker(url = "https://pact-broker.example.com")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class BenutzerServicePactVerificationTest {
@Autowired
private BenutzerRepository repository;
@LocalServerPort
private int port;
@BeforeEach
void setup(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTest(PactVerificationContext context) {
context.verifyInteraction();
}
@State("Benutzer 42 existiert")
void benutzer42Existiert() {
repository.save(new Benutzer(42L, "Max Mustermann", "max@example.com"));
}
}
7.4 Spring Cloud Contract Alternative
// contracts/benutzer/benutzer_finden.groovy
Contract.make {
description "sollte Benutzer zurückgeben"
request {
method GET()
url "/api/benutzer/42"
}
response {
status 200
headers {
contentType applicationJson()
}
body([
id: 42,
name: "Max Mustermann",
email: "max@example.com"
])
}
}
8. Microservice-Architekturen testen
8.1 Die Test-Pyramide für Microservices
▲
/│\
/ │ \
/ │ \ E2E-Tests (wenige, teuer)
/ │ \
/────┼────\
/ │ \
/ │ \ Integration Tests
/ │ \ (Contract Tests,
/ │ \ Testcontainers)
/─────────┼─────────\
/ │ \
/ │ \ Unit Tests (viele,
/ │ \ schnell, Mocks)
/─────────────┴─────────────\
8.2 Service-Übergreifende Integration Tests
@Testcontainers
class BestellWorkflowIntegrationTest {
companion object {
private val network = Network.newNetwork()
@Container
@JvmStatic
val postgres = PostgreSQLContainer("postgres:16-alpine")
.withNetwork(network)
.withNetworkAliases("db")
@Container
@JvmStatic
val kafka = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"))
.withNetwork(network)
.withNetworkAliases("kafka")
@Container
@JvmStatic
val bestellService = GenericContainer("bestellservice:latest")
.withNetwork(network)
.withExposedPorts(8080)
.withEnv(mapOf(
"SPRING_DATASOURCE_URL" to "jdbc:postgresql://db:5432/test",
"SPRING_KAFKA_BOOTSTRAP_SERVERS" to "kafka:9092"
))
.dependsOn(postgres, kafka)
@Container
@JvmStatic
val lagerService = GenericContainer("lagerservice:latest")
.withNetwork(network)
.withExposedPorts(8080)
.withEnv("SPRING_KAFKA_BOOTSTRAP_SERVERS", "kafka:9092")
.dependsOn(kafka)
}
@Test
fun `kompletter Bestellworkflow`() {
// Bestellung aufgeben
val response = given()
.baseUri("http://localhost:${bestellService.firstMappedPort}")
.contentType(ContentType.JSON)
.body("""{"artikelId": 1, "menge": 5}""")
.`when`()
.post("/api/bestellungen")
.then()
.statusCode(201)
.extract()
.response()
val bestellungId = response.jsonPath().getString("id")
// Warten auf Event-Verarbeitung
await()
.atMost(Duration.ofSeconds(30))
.pollInterval(Duration.ofSeconds(1))
.untilAsserted {
given()
.baseUri("http://localhost:${bestellService.firstMappedPort}")
.`when`()
.get("/api/bestellungen/$bestellungId")
.then()
.statusCode(200)
.body("status", equalTo("BESTAETIGT"))
}
}
}
8.3 Chaos Engineering im Test
@Test
fun `sollte graceful degradieren bei Lagerservice-Ausfall`() {
// Lagerservice stoppen
lagerService.stop()
// Bestellung sollte trotzdem angenommen werden (Circuit Breaker)
val response = given()
.baseUri("http://localhost:${bestellService.firstMappedPort}")
.contentType(ContentType.JSON)
.body("""{"artikelId": 1, "menge": 5}""")
.`when`()
.post("/api/bestellungen")
.then()
.statusCode(202) // Accepted, wird später verarbeitet
.body("status", equalTo("AUSSTEHEND"))
.extract()
.response()
// Lagerservice wieder starten
lagerService.start()
// Bestellung sollte nachträglich bestätigt werden
await()
.atMost(Duration.ofMinutes(2))
.untilAsserted { /* ... */ }
}
9. Best Practices
9.1 Die Goldenen Regeln des Mockings
-
Mocke nur, was du nicht kontrollierst
- ✅ Externe APIs, Datenbanken (für Unit Tests)
- ❌ Deine eigenen Klassen im selben Modul
-
Vermeide Mock-Kaskaden
// ❌ Schlecht: Mock-Inception every { serviceA.getB().getC().doThing() } returns result // ✅ Besser: Refactoring oder Integration Test -
Teste Verhalten, nicht Implementierung
// ❌ Schlecht: Fragiler Test verify(exactly = 3) { repository.findById(any()) } // ✅ Besser: Testen des Ergebnisses assertThat(service.processAll()).hasSize(3) -
Nutze Relaxed Mocks sparsam
// MockK relaxed mock - gibt Defaults zurück val mock = mockk<Service>(relaxed = true) // Verwende nur, wenn du wirklich alle Methoden brauchst
9.2 Testdaten-Management
object TestDatenFactory {
fun einKunde(
id: Long = 1L,
name: String = "Max Mustermann",
email: String = "max@example.com"
) = Kunde(id, name, email)
fun eineBestellung(
id: String = UUID.randomUUID().toString(),
kundeId: Long = 1L,
artikel: List<ArtikelPosition> = listOf(einArtikel())
) = Bestellung(id, kundeId, artikel)
fun einArtikel(
artikelId: Long = 100L,
menge: Int = 1,
preis: BigDecimal = BigDecimal("29.99")
) = ArtikelPosition(artikelId, menge, preis)
}
// Verwendung
@Test
fun test() {
val kunde = TestDatenFactory.einKunde(name = "Spezieller Name")
// ...
}
9.3 Test-Slice-Annotationen in Spring Boot
| Annotation | Was wird geladen | Verwendung |
|---|---|---|
@WebMvcTest | Controller-Layer | REST-Controller testen |
@WebFluxTest | Reactive Controller | WebFlux-Endpoints |
@DataJpaTest | JPA-Layer | Repository-Tests |
@DataMongoTest | MongoDB-Layer | MongoDB-Repositories |
@JdbcTest | JDBC-Layer | Rohe SQL-Queries |
@JsonTest | JSON-Serialisierung | Serializer-Tests |
10. CI/CD Integration
10.1 GitHub Actions Beispiel
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- name: Run Unit Tests
run: ./gradlew test --tests '*UnitTest'
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- name: Run Integration Tests
run: ./gradlew test --tests '*IntegrationTest'
contract-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- name: Run Contract Tests
run: ./gradlew contractTest
- name: Publish Pacts
if: github.ref == 'refs/heads/main'
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
run: ./gradlew pactPublish
10.2 Testcontainers Cloud
Für CI/CD-Umgebungen ohne Docker-Zugriff:
// Automatische Erkennung von Testcontainers Cloud
@Testcontainers
class CloudEnabledTest {
// Testcontainers Cloud wird automatisch verwendet,
// wenn die Umgebungsvariablen gesetzt sind
}
# CI/CD Konfiguration
env:
TC_CLOUD_TOKEN: ${{ secrets.TC_CLOUD_TOKEN }}
10.3 Test-Parallelisierung
// build.gradle.kts
tasks.test {
useJUnitPlatform()
// Parallele Ausführung
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2)
.coerceAtLeast(1)
// Separate JVMs für bessere Isolation
forkEvery = 100
}
11. Troubleshooting
11.1 Häufige Probleme
Problem: Kotlin finale Klassen nicht mockbar
// Lösung 1: MockK verwenden
val mock = mockk<FinalClass>()
// Lösung 2: mockito-inline dependency
// build.gradle.kts
testImplementation("org.mockito:mockito-inline:5.x.x")
Problem: Testcontainer startet nicht
// Prüfen ob Docker läuft
@Test
fun dockerVerfuegbar() {
assertTrue(DockerClientFactory.instance().isDockerAvailable)
}
// Logging aktivieren
// logback-test.xml
<logger name="org.testcontainers" level="DEBUG"/>
<logger name="com.github.dockerjava" level="DEBUG"/>
Problem: Flaky Kafka-Tests
// Warte auf Partition-Zuweisung
@BeforeEach
fun setup() {
consumer.subscribe(listOf("topic"))
// Wichtig: Mindestens einmal poll() aufrufen für Assignment
consumer.poll(Duration.ofSeconds(0))
consumer.seekToBeginning(consumer.assignment())
}
11.2 Performance-Tipps
-
Container wiederverwenden
# ~/.testcontainers.properties testcontainers.reuse.enable=true -
Datenbank-Cleanup statt Container-Neustart
@BeforeEach fun cleanup() { // Schneller als Container-Neustart jdbcTemplate.execute("TRUNCATE TABLE bestellungen CASCADE") } -
Test-Slicing nutzen
// Statt @SpringBootTest für alles @DataJpaTest // Lädt nur JPA-relevante Beans class RepositoryTest
12. Tool-Übersicht
| Tool | Verwendung | Empfohlen für |
|---|---|---|
| Mockito | Unit-Test Mocking | Java-Projekte |
| MockK | Unit-Test Mocking | Kotlin-Projekte |
| Testcontainers | Integration Tests | Datenbanken, Broker, Services |
| WireMock | HTTP-API Mocking | Externe Dienste |
| Pact | Contract Testing | API-Verträge |
| Spring Cloud Contract | Contract Testing | Spring-Ökosystem |
| EmbeddedKafka | Kafka Unit Tests | Schnelle Kafka-Tests |
| H2/HSQLDB | In-Memory DB | Schnelle DB-Tests |
Fazit
Das Testen in Cloud-Native-Umgebungen ist komplex, aber mit den richtigen Tools beherrschbar. Die Kunst liegt darin, die richtige Teststrategie für jede Situation zu wählen:
- Unit Tests mit Mocks für Geschäftslogik
- Integration Tests mit Testcontainers für Infrastruktur
- Contract Tests für Service-Grenzen
- E2E Tests (sparsam!) für kritische Workflows
Und denk daran: Der beste Test ist der, der einen Bug findet, bevor er in Produktion geht. Der zweitbeste ist der, der dich nachts schlafen lässt.
Zuletzt aktualisiert: November 2025
Getestet mit: Java 25, Kotlin 2.0, Spring Boot 3.3, Quarkus 3.15, Testcontainers 2.0