Testing-Time-Travel-Serie
Von Dr. Cassian Holt & Jamal Hassan
Java Fleet Systems Consulting, Essen-Rüttenscheid
🔗 Bisher in der Serie
- Prolog – Warum diese Serie anders wird 👥
- Teil 1: Unit Testing Grundlagen – Jamals Production-Disaster 🔧
- Teil 2: Die Test-Pyramide – 70/20/10 erklärt 👥
- Teil 3: TDD & Property-Based Testing – Kontroverse Diskussion 👥
- Teil 4: Mocking & Test Doubles – Die Kunst der Fakes 🔧
- Teil 5: Security & Chaos Engineering – Production-Ready werden 👥
Heute: Teil 6 – Integration Testing mit Spring Boot 🔧
⚡ 30-Sekunden-Zusammenfassung
Integration Testing:
@SpringBootTestrichtig nutzen (ohne 5-Minuten-Tests)- Testcontainers für echte Datenbanken
@DataJpaTestvs.@WebMvcTest– wann was?- Test-Slicing für schnellere Tests
Bottom Line: Integration-Tests sind langsamer als Unit-Tests, aber du brauchst sie trotzdem. Wir zeigen dir, wie du sie schnell UND aussagekräftig machst.
🔧 Hi, Jamal hier!
Moin! Jamal hier. Diese Woche geht’s um Integration-Tests und AI-Testing das Thema, bei dem die meisten Entwickler die Augen verdrehen.
Warum? Weil Integration-Tests oft langsam, flaky und schwer zu debuggen sind. Ich kenne das. Ich hatte mal ein Projekt, wo die Integration-Tests 45 Minuten liefen. Ja, 45 Minuten. Niemand wollte sie laufen lassen. Nach einem Monat waren sie komplett ignoriert.
Heute zeige ich dir, wie du das vermeidest.
Meine Story: Der Tag, an dem ich Integration-Tests schätzen lernte
2021, kurz nachdem ich zu Java Fleet kam. Wir hatten eine Spring Boot App mit einer PostgreSQL-Datenbank. Ich hatte super Unit-Tests geschrieben. Alle grün. Alles gut.
Dann deployen wir nach Staging. Und die App startet nicht. BeanCreationException. InvalidDataAccessApiUsageException. Chaos.
Was war das Problem?
Meine Unit-Tests hatten alles gemockt. Ich hatte nie getestet, ob meine JPA-Repositories wirklich mit PostgreSQL funktionieren. Oder ob meine Spring-Configuration korrekt war. Oder ob die Transaktionen richtig laufen.
Der Lernmoment:
Unit-Tests testen deine Logik. Integration-Tests testen, ob deine Komponenten zusammenspielen. Beides ist wichtig.
Seitdem: Jedes Feature bekommt Unit-Tests UND Integration-Tests. Und die Integration-Tests laufen in unter 2 Minuten.
📊 Integration Testing 101: Was sind Integration-Tests?
Definition (Jamal-Style)
Unit-Test: „Funktioniert meine Klasse isoliert?“
Integration-Test: „Funktionieren meine Klassen zusammen?“
Praktisches Beispiel:
// Unit-Test: TaskService isoliert testen
@ExtendWith(MockitoExtension.class)
class TaskServiceUnitTest {
@Mock
private TaskRepository taskRepository;
@Mock
private EmailService emailService;
@InjectMocks
private TaskService taskService;
@Test
void shouldCreateTask() {
// Alles gemockt - sehr schnell!
when(taskRepository.save(any())).thenReturn(testTask);
Task result = taskService.createTask(request);
assertThat(result).isNotNull();
}
}
// Integration-Test: TaskService + Repository + Database
@SpringBootTest
@Transactional
class TaskServiceIntegrationTest {
@Autowired
private TaskService taskService;
@Autowired
private TaskRepository taskRepository;
@Test
void shouldSaveTaskToRealDatabase() {
// Nichts gemockt - echte DB!
CreateTaskRequest request = new CreateTaskRequest("Test", "Description");
Task created = taskService.createTask(request);
// Verify: Task ist wirklich in der DB
Task fromDb = taskRepository.findById(created.getId()).orElseThrow();
assertThat(fromDb.getTitle()).isEqualTo("Test");
}
}
Der Unterschied:
- Unit-Test: 10ms, findet Logik-Bugs
- Integration-Test: 500ms, findet Integration-Bugs
Wann brauchst du Integration-Tests?
Immer wenn:
- ✅ Du mit einer Datenbank arbeitest
- ✅ Du externe Services callst
- ✅ Du Spring-Configuration testest
- ✅ Du Transaktions-Verhalten prüfst
- ✅ Du Security-Setup validierst
Nicht nötig wenn:
- ❌ Pure Business-Logik (Unit-Tests reichen)
- ❌ Einfache Utilities (zu schnell für Integration)
- ❌ Algorithmen ohne Dependencies
🚀 @SpringBootTest: Der Complete Spring Context
Das Basics-Setup
@SpringBootTest
class TaskControllerIntegrationTest {
@Autowired
private TaskController controller;
@Test
void contextLoads() {
// Test 1: Spring Context startet überhaupt
assertThat(controller).isNotNull();
}
}
Was passiert hier?
- Spring Boot startet KOMPLETT (alle Beans, ganze Configuration)
- Datenbank wird hochgefahren (H2 in-memory per default)
- Controller, Services, Repositories – alles echt
- Dauert 3-5 Sekunden
Das Problem: Zu langsam!
Cassian würde jetzt sagen: „Das ist thermodynamisch ineffizient!“
Ich sage: „Wenn jeder Test 5 Sekunden braucht, läuft keiner sie mehr.“
Die Lösung: Test Slicing
✂️ Test Slicing: Nur laden, was du brauchst
Spring Boot bietet spezialisierte Test-Annotationen, die nur Teile der App laden.
@WebMvcTest – Nur Controller testen
@WebMvcTest(TaskController.class)
class TaskControllerWebTest {
@Autowired
private MockMvc mockMvc;
@MockBean // Dieser Service wird gemockt!
private TaskService taskService;
@Test
void shouldReturnTasksViaHttp() throws Exception {
// Arrange
Task task = new Task(1L, "Test Task");
when(taskService.getAllTasks()).thenReturn(List.of(task));
// Act & Assert
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Test Task"));
}
}
Was wird geladen:
- ✅ Web Layer (Controller, Request/Response Mapping)
- ✅ MockMvc für HTTP-Testing
- ❌ Services (müssen gemockt werden)
- ❌ Datenbank
- ❌ Security (außer du aktivierst es)
Speed: ~1 Sekunde statt 5
@DataJpaTest – Nur Repository testen
@DataJpaTest
class TaskRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private TaskRepository taskRepository;
@Test
void shouldFindTasksByStatus() {
// Arrange: Task in DB schreiben
Task task = new Task("Test", TaskStatus.PENDING);
entityManager.persistAndFlush(task);
// Act: Repository-Methode aufrufen
List<Task> pending = taskRepository.findByStatus(TaskStatus.PENDING);
// Assert
assertThat(pending).hasSize(1);
assertThat(pending.get(0).getTitle()).isEqualTo("Test");
}
}
Was wird geladen:
- ✅ JPA Repositories
- ✅ In-Memory H2 Database
- ✅ Transaction Management
- ❌ Services
- ❌ Controller
- ❌ Security
Speed: ~800ms
Jamal’s Faustregel: Wann welche Annotation?
| Du testest… | Verwende… | Warum? |
|---|---|---|
| REST-Endpoints | @WebMvcTest | Nur Web Layer, schnell |
| Database Queries | @DataJpaTest | Nur Repository, schnell |
| Service + DB | @SpringBootTest | Voller Context nötig |
| Security Config | @SpringBootTest | Voller Context nötig |
| Async Processing | @SpringBootTest | Voller Context nötig |
🐳 Testcontainers: Echte Datenbanken für Tests
Das Problem mit H2
// Test läuft mit H2
@DataJpaTest
class TaskRepositoryTest {
@Test
void shouldFindByComplexQuery() {
// Test ist grün mit H2
List<Task> tasks = taskRepository.findByComplexPostgreSQLQuery();
assertThat(tasks).hasSize(5);
}
}
Dann in Production mit PostgreSQL:
ERROR: syntax error at or near "LIMIT"
Warum? Weil H2 nicht 100% PostgreSQL-kompatibel ist. Especially bei:
- Native Queries
- Database-spezifischen Features
- JSON-Columns
- Full-Text-Search
Die Lösung: Testcontainers
Testcontainers startet einen echten PostgreSQL Docker-Container für deine Tests.
Setup (Maven):
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
Verwendung:
@SpringBootTest
@Testcontainers
class TaskRepositoryPostgresTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private TaskRepository taskRepository;
@Test
void shouldWorkWithRealPostgreSQL() {
// Jetzt mit ECHTEM PostgreSQL!
Task task = new Task("Test", "Description");
Task saved = taskRepository.save(task);
assertThat(saved.getId()).isNotNull();
// Native PostgreSQL Query funktioniert
List<Task> result = taskRepository.findByPostgreSQLSpecificQuery();
assertThat(result).isNotEmpty();
}
}
Was passiert:
- Test startet → Docker Container wird hochgefahren
- PostgreSQL läuft (~5 Sekunden beim ersten Mal)
- Spring verbindet sich mit Container
- Test läuft gegen ECHTE PostgreSQL
- Test endet → Container wird gestoppt
Performance-Trick:
@SpringBootTest
@Testcontainers
class FastPostgresTest {
// static = Container wird für ALLE Tests wiederverwendet!
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withReuse(true); // Container bleibt nach Tests am Leben
// Erste Test-Klasse: 5 Sekunden
// Weitere Test-Klassen: <1 Sekunde (Container-Reuse!)
}
Jamal’s Testcontainers-Strategie
Meine Regel:
- Unit-Tests: Keine DB
- Repository-Tests:
@DataJpaTestmit H2 (schnell) - Integration-Tests (kritische Queries): Testcontainers mit PostgreSQL
- E2E-Tests: Testcontainers
Warum nicht alles mit Testcontainers?
Weil 100 Tests mit Testcontainers = 10 Minuten. 100 Tests mit H2 = 30 Sekunden.
Use Testcontainers nur für Tests, wo H2 nicht ausreicht.
🌐 HTTP-Testing mit MockMvc
REST-Endpoints testen ohne Server
@SpringBootTest
@AutoConfigureMockMvc
class TaskApiIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private TaskRepository taskRepository;
@Test
@WithMockUser(username = "test", roles = "USER")
void shouldCreateTaskViaApi() throws Exception {
// Arrange
String taskJson = """
{
"title": "New Task",
"description": "Test Description",
"status": "PENDING"
}
""";
// Act
MvcResult result = mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(taskJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.title").value("New Task"))
.andReturn();
// Assert: Task ist in DB
String responseBody = result.getResponse().getContentAsString();
Long taskId = JsonPath.read(responseBody, "$.id");
Task saved = taskRepository.findById(taskId).orElseThrow();
assertThat(saved.getTitle()).isEqualTo("New Task");
}
@Test
@WithMockUser(username = "test", roles = "USER")
void shouldReturn404ForNonExistentTask() throws Exception {
mockMvc.perform(get("/api/tasks/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("Task not found"));
}
}
Was ich hier mag:
- Testet HTTP Layer (Request Mapping, Status Codes, JSON)
- Testet Security (mit
@WithMockUser) - Validiert DB-Interaktion
- Kein echter Server nötig (MockMvc simuliert)
<details> <summary>📚 <strong>Cassian’s Deep-Dive: Die Thermodynamik von Integration-Tests</strong></summary>
Cassian hier.
Jamal hat vorhin „thermodynamisch ineffizient“ gesagt. Das war nur halb-ironisch. Lass mich das erklären.
Test-Execution als thermodynamisches System:
Unit-Tests sind wie isolierte Partikel: O(1) Komplexität. Jeder Test ist unabhängig, vorhersagbar, deterministisch.
Integration-Tests sind wie interagierende Systeme: O(n) bis O(n²) Komplexität. Je mehr Komponenten, desto mehr potenzielle Interaktionen.
Die Mathematik:
Wenn du N Komponenten hast:
- Unit-Tests: N Tests (linear)
- Integration-Tests: N×(N-1)/2 potenzielle Interaktionen (quadratisch!)
Deshalb Jamal’s 70/20/10-Verteilung: Unit-Tests skalieren linear, Integration-Tests nicht.
Das Spring-Context-Problem:
Spring Boot Context-Loading ist wie ein Phasenübergang in der Physik. Es braucht Energie (Zeit), den ganzen Context zu initialisieren. Test Slicing (@WebMvcTest, @DataJpaTest) ist wie Teil-System-Analyse: Du testest Subsysteme statt das Gesamtsystem.
Thermodynamisch optimal: Minimize State, Maximize Information. Das ist Test Slicing. </details>
🎯 Praxis: Complete Integration-Test-Suite
Hier ist, wie ich eine komplette Feature-Test-Suite aufbaue:
Feature: Task Management
// 1. Unit-Tests für Business-Logik (70%)
@ExtendWith(MockitoExtension.class)
class TaskServiceUnitTest {
@Mock TaskRepository repository;
@Mock EmailService emailService;
@InjectMocks TaskService taskService;
@Test
void shouldValidateTaskTitle() {
// Business-Logik isoliert testen
}
@Test
void shouldSendEmailOnTaskCompletion() {
// Email-Integration gemockt
}
}
// 2. Repository-Tests mit H2 (15%)
@DataJpaTest
class TaskRepositoryTest {
@Autowired TaskRepository repository;
@Autowired TestEntityManager entityManager;
@Test
void shouldFindTasksByStatus() {
// Schnelle Repository-Tests mit H2
}
}
// 3. Integration-Tests mit Testcontainers (10%)
@SpringBootTest
@Testcontainers
class TaskServicePostgresIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = /*...*/;
@Autowired TaskService taskService;
@Test
void shouldHandleComplexTransactionScenarios() {
// Kritische Szenarien mit echter DB
}
}
// 4. API-Tests mit MockMvc (5%)
@SpringBootTest
@AutoConfigureMockMvc
class TaskApiIntegrationTest {
@Autowired MockMvc mockMvc;
@Test
@WithMockUser
void shouldCreateTaskViaApi() {
// E2E für kritische User-Flows
}
}
Resultat:
- 50 Unit-Tests: 5 Sekunden
- 10 Repository-Tests: 3 Sekunden
- 5 Integration-Tests mit Testcontainers: 15 Sekunden (Container-Reuse!)
- 3 API-Tests: 5 Sekunden
Total: 28 Sekunden für 68 Tests
💡 Action Items – Deine Integration-Test-Strategie
Level 1: Basics (Diese Woche)
- [ ] Einen
@SpringBootTestfür deine wichtigste Feature schreiben - [ ]
@DataJpaTestfür dein komplexestes Repository - [ ] MockMvc für deinen wichtigsten REST-Endpoint
Level 2: Optimization (Nächste Woche)
- [ ] Test Slicing einführen (
@WebMvcTest,@DataJpaTest) - [ ] Testcontainers Setup für PostgreSQL/MySQL
- [ ] Test-Execution-Zeit messen und optimieren
Level 3: Mastery (Nächster Monat)
- [ ] Komplette Integration-Test-Suite für ein Feature
- [ ] Container-Reuse für schnellere Tests
- [ ] CI/CD Integration mit parallelen Tests
💬 Cassian & Jamal’s Diskussion
Cassian: Jamal, wie entscheidest du, ob ein Test ein Unit-Test oder Integration-Test sein soll?
Jamal: Gute Frage. Meine Regel: Wenn ich mehr als eine echte Komponente brauche, ist es Integration. Aber ich fange immer mit Unit-Tests an.
Cassian: Interessant. Aus theoretischer Sicht ist die Grenze fließend. Michael Feathers definiert Unit-Tests als „Tests, die schnell laufen, isoliert sind und keine externe Dependencies haben“. Alles andere ist Integration.
Jamal: Das ist mir zu abstrakt. Ich denke pragmatisch: Brauche ich Spring? Brauche ich eine DB? Dann Integration. Sonst Unit.
Cassian: Fair enough. Aber was machst du mit Services, die andere Services callen?
Jamal: Mocken. Wenn Service A Service B callt, dann mocke ich B im Unit-Test von A. Im Integration-Test lasse ich beide echt laufen.
Cassian: Und wenn Service B eine kritische Dependency ist?
Jamal: Dann schreibe ich einen zusätzlichen Integration-Test. Aber Unit-Tests bleiben gemockt. Das ist der Unterschied: Unit = isoliert + schnell. Integration = realistisch + langsamer.
Cassian: Das ist methodisch sauber. Test-Pyramide in Aktion.
Jamal: Genau.
❓ FAQ
1. @SpringBootTest vs. @DataJpaTest – wann was?
Jamal: Einfach: @DataJpaTest wenn du nur Repositories testest. @SpringBootTest wenn du Services + Repositories + mehr brauchst. DataJpaTest ist 5x schneller.
2. Muss ich Testcontainers verwenden?
Cassian: Nicht zwingend, aber empfohlen für Production-nahe Tests. H2 ist okay für einfache Queries, aber native SQL oder DB-spezifische Features brauchest du echte Datenbanken.
Jamal: Meine Regel: 80% mit H2, 20% mit Testcontainers für kritische Queries.
3. Wie halte ich Integration-Tests schnell?
Jamal:
- Test Slicing (
@WebMvcTest,@DataJpaTest) - Container-Reuse für Testcontainers
- Parallele Execution mit Maven/Gradle
- Nur kritische Szenarien als Integration-Tests
4. @Transactional in Tests – Ja oder Nein?
Cassian: Per default macht @DataJpaTest automatisch Rollback nach jedem Test. Bei @SpringBootTest musst du @Transactional manuell setzen, wenn du Rollback willst.
Jamal: Ich nutze @Transactional fast immer. Tests sollen die DB nicht verschmutzen. Ausnahme: Wenn ich explizit Commit-Verhalten testen will.
5. MockMvc vs. TestRestTemplate – was ist besser?
Jamal: MockMvc für @WebMvcTest (schneller, kein Server). TestRestTemplate für @SpringBootTest mit richtigem Server. Ich bevorzuge MockMvc, weil schneller.
6. Wie teste ich Async-Processing?
Cassian: Mit @Async brauchst du @SpringBootTest und Awaitility:
@Test
void shouldProcessAsync() {
taskService.processAsync(task);
await().atMost(5, SECONDS)
.untilAsserted(() -> {
Task result = taskRepository.findById(task.getId()).orElseThrow();
assertThat(result.getStatus()).isEqualTo(TaskStatus.COMPLETED);
});
}
7. Was ist mit Flaky Integration-Tests?
Jamal: Die häufigsten Gründe:
- Timing-Probleme (Async ohne await)
- Shared State zwischen Tests (fehlende
@Transactional) - Port-Konflikte (Testcontainers-Port-Collision)
- Netzwerk-Timeouts
Lösung: Transactional Rollback, await() für Async, Container-Reuse.
8. Manchmal denke ich, Integration-Tests sind mehr Arbeit als sie wert sind. Aber dann erinnere ich mich an Production-Bugs, die nur Integration-Tests gefunden hätten…
Falls du dich fragst, wie wir mit Testing-Frust umgehen – manche Geschichten gehören in private logs. Aber hey, wenn du neugierig bist: Unsere Website-Suche findet mehr als nur Code-Patterns. 🔍
📝 Zusammenfassung
Integration-Testing:
- Teste Komponenten-Interaktionen, nicht nur isolierte Logik
- Spring bietet Test Slicing für schnellere Tests
- Testcontainers für echte Datenbanken bei kritischen Tests
- MockMvc für HTTP-Layer ohne Server
Jamal’s Bottom Line:
Integration-Tests fühlen sich langsam an. Aber mit Test Slicing, Container-Reuse und smarter Strategie (70/20/10) sind sie handhabbar. Und sie fangen Bugs, die Unit-Tests niemals finden.
Cassian’s Bottom Line:
Integration-Testing ist empirische System-Analyse. Du testest emergente Eigenschaften komplexer Systeme. Das ist methodisch notwendig, auch wenn es thermodynamisch teurer ist als Unit-Tests.
🔮 Nächste Woche: Teil 7 – Das Testing-Mastery-Toolkit
Beide übernehmen 👥
Das große Finale:
- 🎁 Complete Testing Cheat Sheet (PDF Download)
- 📊 Test-Strategy Decision Tree
- 🛠️ CI/CD Integration Guide
- 🏆 Java Fleet Testing Certificate
Plus: Die ultimative Testing-Ressourcen-Sammlung!
Cassian: „Wir packen alles zusammen, was du für Testing-Mastery brauchst.“
Jamal: „Und ich zeige dir, wie du das im echten Projekt umsetzt. Pragmatisch, nicht theoretisch.“
Cassian: „Fair enough.“
Bis nächste Woche! 👋
📚 Serie-Übersicht
- Teil 0: Prolog – Cassian & Jamal stellen sich vor 👥
- Teil 1: Unit Testing Grundlagen – Jamals Production-Disaster 🔧
- Teil 2: Die Test-Pyramide – 70/20/10 erklärt 👥
- Teil 3: TDD & Property-Based Testing – Kontroverse Diskussion 👥
- Teil 4: Mocking & Test Doubles – Die Kunst der Fakes 🔧
- Teil 5: Security & Chaos Engineering – Production-Ready werden 👥
- Teil 6: Integration Testing – Spring Boot Tests optimieren 🔧 ← Du bist hier
- Teil 7: Testing-Mastery-Toolkit – Coming soon 👥
Java Fleet Systems Consulting, Essen-Rüttenscheid
Pragmatische Software-Entwicklung mit wissenschaftlichem Fundament
P.S. von Jamal: Integration-Tests haben mir mehr Production-Bugs erspart als ich zählen kann. Der Invest lohnt sich. Versprochen.
P.P.S. von Cassian: Die Test-Pyramide ist nicht nur Konvention – sie ist mathematisch optimal für Test-Suites. Die Thermodynamik lügt nicht.

