meldestelle/.junie/guidelines/project-standards/testing-standards.md
2025-09-15 12:49:55 +02:00

11 KiB

Testing Standards und Qualitätssicherung


guideline_type: "project-standards" scope: "testing-standards" audience: ["developers", "ai-assistants"] last_updated: "2025-09-15" dependencies: ["master-guideline.md", "coding-standards.md"] related_files: ["build.gradle.kts", "src/test/**", "testcontainers.properties"] ai_context: "Testing strategies, test pyramid, tools, coverage requirements, and debugging practices"

🧪 Testing Standards

Tests sind ein integraler Bestandteil jedes Features und müssen einen hohen Standard erfüllen.

🤖 AI-Assistant Hinweis: Testing-Prinzipien für das Meldestelle-Projekt:

  • Test-Pyramide: 80%+ Unit-Tests, Integrationstests für externe Systeme
  • Testcontainers: Goldstandard für Infrastruktur-Tests
  • Debug-Logs: Präfix [DEBUG_LOG] für Test-Ausgaben
  • Result-Pattern: Tests müssen auch Error-Handling validieren

Test-Pyramide & Werkzeuge

Unit-Tests (80 %+ Abdeckung)

Für Domänen- und Anwendungslogik (JUnit 5, MockK).

class MemberServiceTest {
    private val memberRepository = mockk<MemberRepository>()
    private val eventPublisher = mockk<EventPublisher>()
    private val memberService = MemberService(memberRepository, eventPublisher)

    @Test
    fun `should return Success when member is created successfully`() {
        // Given
        val command = CreateMemberCommand(
            memberId = MemberId.generate(),
            name = "Max Mustermann",
            email = "max@example.com"
        )

        every { memberRepository.save(any()) } returns Result.Success(Unit)
        every { eventPublisher.publish(any()) } returns Result.Success(Unit)

        // When
        val result = memberService.createMember(command)

        // Then
        assertThat(result).isInstanceOf<Result.Success<Unit>>()
        verify { memberRepository.save(any()) }
        verify { eventPublisher.publish(ofType<MemberCreatedEvent>()) }
    }

    @Test
    fun `should return Failure when repository save fails`() {
        // Given
        val command = CreateMemberCommand(
            memberId = MemberId.generate(),
            name = "Max Mustermann",
            email = "max@example.com"
        )

        every { memberRepository.save(any()) } returns Result.Failure(RepositoryError.DATABASE_ERROR)

        // When
        val result = memberService.createMember(command)

        // Then
        assertThat(result).isInstanceOf<Result.Failure<RepositoryError>>()
        verify { memberRepository.save(any()) }
        verify(exactly = 0) { eventPublisher.publish(any()) }
    }
}

Integrationstests

Decken alle Repository-Implementierungen und externen Integrationen ab.

@Testcontainers
class MemberRepositoryIntegrationTest {

    @Container
    private val postgresContainer = PostgreSQLContainer("postgres:16-alpine")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test")

    private lateinit var memberRepository: MemberRepository

    @BeforeEach
    fun setup() {
        val dataSource = HikariDataSource().apply {
            jdbcUrl = postgresContainer.jdbcUrl
            username = postgresContainer.username
            password = postgresContainer.password
        }

        // Run migrations
        Flyway.configure()
            .dataSource(dataSource)
            .locations("db/migration")
            .load()
            .migrate()

        memberRepository = PostgresMemberRepository(dataSource)
    }

    @Test
    fun `should save and retrieve member successfully`() {
        // Given
        val member = Member(
            id = MemberId.generate(),
            name = "Integration Test Member",
            email = "integration@test.com"
        )

        // When
        val saveResult = runBlocking { memberRepository.save(member) }
        val findResult = runBlocking { memberRepository.findById(member.id) }

        // Then
        assertThat(saveResult).isInstanceOf<Result.Success<Unit>>()
        assertThat(findResult).isInstanceOf<Result.Success<Member?>>()

        val retrievedMember = (findResult as Result.Success).value
        assertThat(retrievedMember?.id).isEqualTo(member.id)
        assertThat(retrievedMember?.name).isEqualTo(member.name)
        assertThat(retrievedMember?.email).isEqualTo(member.email)
    }
}

Testcontainers als Goldstandard

Jede Interaktion mit externer Infrastruktur (DB, Cache, Broker) muss mit Testcontainers getestet werden.

@Testcontainers
class EventStoreIntegrationTest {

    companion object {
        @Container
        @JvmStatic
        private val redisContainer = GenericContainer<Nothing>("redis:7-alpine")
            .withExposedPorts(6379)

        @Container
        @JvmStatic
        private val kafkaContainer = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
    }

    @Test
    fun `should store and retrieve events from Redis`() {
        println("[DEBUG_LOG] Testing Redis event storage")

        // Given
        val eventStore = RedisEventStore(
            redisHost = redisContainer.host,
            redisPort = redisContainer.getMappedPort(6379)
        )

        val event = MemberCreatedEvent(
            memberId = MemberId.generate(),
            name = "Test Member",
            timestamp = Instant.now()
        )

        // When
        val storeResult = runBlocking { eventStore.store(event) }
        val retrieveResult = runBlocking { eventStore.getEvents(event.memberId) }

        // Then
        assertThat(storeResult).isInstanceOf<Result.Success<Unit>>()
        assertThat(retrieveResult).isInstanceOf<Result.Success<List<DomainEvent>>>()

        val events = (retrieveResult as Result.Success).value
        assertThat(events).hasSize(1)
        assertThat(events.first()).isInstanceOf<MemberCreatedEvent>()

        println("[DEBUG_LOG] Successfully stored and retrieved ${events.size} events")
    }
}

Debugging in Tests

Debug-Ausgaben im Test-Code müssen mit [DEBUG_LOG] beginnen, um sie leicht identifizieren und filtern zu können.

@Test
fun `should handle complex business scenario`() {
    println("[DEBUG_LOG] Starting complex business scenario test")

    // Test implementation

    println("[DEBUG_LOG] Member created with ID: ${member.id}")
    println("[DEBUG_LOG] Published ${events.size} domain events")
    println("[DEBUG_LOG] Test completed successfully")
}

🎯 AI-Assistenten: Testing-Schnellreferenz

Test-Kategorien und Werkzeuge

Test-Typ Coverage-Ziel Werkzeuge Verwendung
Unit-Tests 80%+ JUnit 5, MockK, AssertJ Domänen- & Anwendungslogik
Integrationstests Alle Repositories Testcontainers, JUnit 5 Externe Integrationen
End-to-End Tests Kritische User-Journeys Testcontainers, REST Assured Vollständige Workflows

Testcontainer-Konfiguration

PostgreSQL

@Container
private val postgresContainer = PostgreSQLContainer("postgres:16-alpine")
    .withDatabaseName("testdb")
    .withUsername("test")
    .withPassword("test")
    .withInitScript("test-data.sql")

Redis

@Container
private val redisContainer = GenericContainer<Nothing>("redis:7-alpine")
    .withExposedPorts(6379)
    .withCommand("redis-server", "--appendonly", "yes")

Kafka

@Container
private val kafkaContainer = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
    .withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true")

Keycloak

@Container
private val keycloakContainer = KeycloakContainer("quay.io/keycloak/keycloak:26.0.7")
    .withRealmImportFile("test-realm.json")
    .withAdminUsername("admin")
    .withAdminPassword("admin")

Test-Patterns für Result-Handling

// Success-Case testen
@Test
fun `should return Success when operation succeeds`() {
    // Given
    every { dependency.operation() } returns Result.Success(expectedValue)

    // When
    val result = serviceUnderTest.performOperation()

    // Then
    assertThat(result).isInstanceOf<Result.Success<ExpectedType>>()
    assertThat((result as Result.Success).value).isEqualTo(expectedValue)
}

// Failure-Case testen
@Test
fun `should return Failure when dependency fails`() {
    // Given
    every { dependency.operation() } returns Result.Failure(ExpectedError.SOME_ERROR)

    // When
    val result = serviceUnderTest.performOperation()

    // Then
    assertThat(result).isInstanceOf<Result.Failure<ExpectedError>>()
    assertThat((result as Result.Failure).error).isEqualTo(ExpectedError.SOME_ERROR)
}

Mock-Setup für Services

class ServiceTest {
    private val repository = mockk<Repository>()
    private val eventPublisher = mockk<EventPublisher>()
    private val externalService = mockk<ExternalService>()

    private val serviceUnderTest = Service(repository, eventPublisher, externalService)

    @BeforeEach
    fun setup() {
        clearAllMocks()

        // Default mocks
        every { eventPublisher.publish(any()) } returns Result.Success(Unit)
    }

    @AfterEach
    fun cleanup() {
        confirmVerified(repository, eventPublisher, externalService)
    }
}

Testdaten-Builder

class MemberTestDataBuilder {
    private var id: MemberId = MemberId.generate()
    private var name: String = "Test Member"
    private var email: String = "test@example.com"
    private var status: MemberStatus = MemberStatus.ACTIVE

    fun withId(id: MemberId) = apply { this.id = id }
    fun withName(name: String) = apply { this.name = name }
    fun withEmail(email: String) = apply { this.email = email }
    fun withStatus(status: MemberStatus) = apply { this.status = status }

    fun build() = Member(
        id = id,
        name = name,
        email = email,
        status = status
    )
}

// Verwendung in Tests
@Test
fun `should validate member data`() {
    val member = MemberTestDataBuilder()
        .withName("Max Mustermann")
        .withEmail("max@meldestelle.at")
        .withStatus(MemberStatus.PENDING)
        .build()

    // Test implementation
}

Performance-Tests

@Test
fun `should handle high load efficiently`() {
    println("[DEBUG_LOG] Starting performance test with 1000 concurrent operations")

    val operations = (1..1000).map {
        async {
            serviceUnderTest.performOperation(
                TestCommand(id = MemberId.generate())
            )
        }
    }

    val results = runBlocking {
        operations.awaitAll()
    }

    val successCount = results.count { it is Result.Success }
    val failureCount = results.count { it is Result.Failure }

    println("[DEBUG_LOG] Performance test completed: $successCount successes, $failureCount failures")

    assertThat(successCount).isGreaterThan(950) // 95% success rate minimum
}

Navigation: