Testen in Java: Unit, Integration, E2E — Fortgeschrittenes Niveau
In der modernen Java-Entwicklung ist Testen nicht nur eine Formalität, sondern eine grundlegende Praxis, die Stabilität und Vorhersagbarkeit des Codes gewährleistet. Einfache Tests wie @Test mit Assert.assertEquals sind längst komplexen, mehrschichtigen Strategien gewichen. In diesem Artikel analysieren wir fortgeschrittene Techniken für jede Ebene der Testpyramide: Unit, Integration und End-to-End (E2E). Sie erfahren, wie Sie wartbare, schnelle und zuverlässige Tests schreiben, die Ihren Code in eine gut abgestimmte Maschine verwandeln.
1. Fortgeschrittene Unit-Tests: Mocks, Stubs und Parametrisierung
Unit-Tests überprüfen ein isoliertes Stück Code (normalerweise eine Klasse oder Methode). Auf fortgeschrittenem Niveau bewegen wir uns von einfachen Szenarien zu komplexen Interaktionen und Grenzfällen. Das wichtigste Werkzeug hierfür ist die Bibliothek Mockito.
1.1. Verwendung von ArgumentCaptor und komplexen Antworten (Answer)
Oft ist es notwendig, nicht nur zu überprüfen, ob eine Methode aufgerufen wurde, sondern auch welche Argumente ihr übergeben wurden. Dafür wird ArgumentCaptor verwendet.
// Beispiel: Überprüfung, ob die richtige Nachricht an den Dienst gesendet wurde@ExtendWith(MockitoExtension.class)public class NotificationServiceTest { @Mock private EmailSender emailSender; @InjectMocks private NotificationService notificationService;
@Test void shouldSendEmailWithCorrectContent() { // Arrange User user = new User("test@example.com", "Test User"); // Act notificationService.sendWelcomeEmail(user); // Assert ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class); verify(emailSender).send(captor.capture()); EmailMessage capturedMessage = captor.getValue(); assertEquals("test@example.com", capturedMessage.getTo()); assertTrue(capturedMessage.getBody().contains("Test User")); }}Für die dynamische Erstellung von Antworten basierend auf Eingabedaten verwenden Sie das Interface Answer:
@Testvoid testDynamicResponse() { when(dataRepository.findById(anyLong())).thenAnswer(invocation -> { Long id = invocation.getArgument(0); return new DataEntity(id, "Entity with ID: " + id); }); assertEquals("Entity with ID: 5", dataService.getDataById(5L).getName());}1.2. Parametrisierte Tests (JUnit 5)
Parametrisierung ermöglicht es, denselben Test mit verschiedenen Datensätzen auszuführen und so Code-Duplizierung zu vermeiden. JUnit 5 bietet leistungsstarke Annotationen wie @CsvSource und @MethodSource.
@ParameterizedTest@CsvSource({ "10, 20, 30", "0, 0, 0", "-5, 5, 0"})void testSum(int a, int b, int expectedSum) { Calculator calculator = new Calculator(); assertEquals(expectedSum, calculator.add(a, b));}2. Integrationstests: Arbeiten mit Datenbanken und externen Diensten
Integrationstests überprüfen die Interaktion zwischen Komponenten des Systems, z. B. mit einer echten Datenbank oder Warteschlangen. Die größte Herausforderung ist die Verwaltung des Umgebungszustands. Hier kommt Testcontainers zur Hilfe.
2.1. Testcontainers für isolierte Datenbanken
Testcontainers startet Docker-Container mit der erforderlichen Software (PostgreSQL, Redis, Kafka) direkt während der Tests und gewährleistet so Sauberkeit und Reproduzierbarkeit.
@SpringBootTest@Testcontainersclass UserRepositoryIntegrationTest {
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15") .withDatabaseName("testdb") .withUsername("test") .withPassword("test");
@DynamicPropertySource static void configureProperties(DynamicPropertyR