Mocking im Cloud-Native-Zeitalter: Ein Leitfaden für Entwickler, die noch Zeit zum Leben haben wollen

Von Boris Sander2. Dezember 2025
Mocking im Cloud-Native-Zeitalter: Ein Leitfaden für Entwickler, die noch Zeit zum Leben haben wollen

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:

FeatureMockitoMockK
Finale Klassen mockenErfordert mockito-inline✅ Nativ
Extension Functions❌ Nicht möglich✅ Mit mockkStatic
CoroutinesUmständlichcoEvery, 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

  1. Mocke nur, was du nicht kontrollierst

    • ✅ Externe APIs, Datenbanken (für Unit Tests)
    • ❌ Deine eigenen Klassen im selben Modul
  2. Vermeide Mock-Kaskaden

    // ❌ Schlecht: Mock-Inception
    every { serviceA.getB().getC().doThing() } returns result
    
    // ✅ Besser: Refactoring oder Integration Test
    
  3. Teste Verhalten, nicht Implementierung

    // ❌ Schlecht: Fragiler Test
    verify(exactly = 3) { repository.findById(any()) }
    
    // ✅ Besser: Testen des Ergebnisses
    assertThat(service.processAll()).hasSize(3)
    
  4. 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

AnnotationWas wird geladenVerwendung
@WebMvcTestController-LayerREST-Controller testen
@WebFluxTestReactive ControllerWebFlux-Endpoints
@DataJpaTestJPA-LayerRepository-Tests
@DataMongoTestMongoDB-LayerMongoDB-Repositories
@JdbcTestJDBC-LayerRohe SQL-Queries
@JsonTestJSON-SerialisierungSerializer-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

  1. Container wiederverwenden

    # ~/.testcontainers.properties
    testcontainers.reuse.enable=true
    
  2. Datenbank-Cleanup statt Container-Neustart

    @BeforeEach
    fun cleanup() {
        // Schneller als Container-Neustart
        jdbcTemplate.execute("TRUNCATE TABLE bestellungen CASCADE")
    }
    
  3. Test-Slicing nutzen

    // Statt @SpringBootTest für alles
    @DataJpaTest  // Lädt nur JPA-relevante Beans
    class RepositoryTest
    

12. Tool-Übersicht

ToolVerwendungEmpfohlen für
MockitoUnit-Test MockingJava-Projekte
MockKUnit-Test MockingKotlin-Projekte
TestcontainersIntegration TestsDatenbanken, Broker, Services
WireMockHTTP-API MockingExterne Dienste
PactContract TestingAPI-Verträge
Spring Cloud ContractContract TestingSpring-Ökosystem
EmbeddedKafkaKafka Unit TestsSchnelle Kafka-Tests
H2/HSQLDBIn-Memory DBSchnelle 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