Skip to content

Commit 47df1bd

Browse files
committed
adopt docs
1 parent ee72550 commit 47df1bd

5 files changed

Lines changed: 403 additions & 389 deletions

File tree

README.md

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ Proudly presented by [PragmaTech GmbH](https://pragmatech.digital/).
88

99
## Workshop Overview
1010

11-
This workshop is designed to demystify testing in Spring Boot applications through hands-on exercises, covering everything from basic unit testing to advanced integration testing techniques, test optimization, and CI/CD best practices. The workshop is divided into eight lab sessions across two days, each focusing on different aspects of testing Spring Boot applications.
11+
This workshop is designed to demystify testing in Spring Boot applications through hands-on exercises, covering everything from basic unit testing to advanced integration testing techniques, test optimization, and CI/CD best practices.
12+
13+
The workshop is divided into eight lab sessions across two days, each focusing on different aspects of testing Spring Boot applications.
1214

1315
### Day One: Testing Spring Boot Applications Demystified
1416

15-
Goal: Getting started with testing Spring Boot applications and learning how to write tests for Spring Boot applications.
17+
**Goal**: Getting started with testing Spring Boot applications and learning how to write tests for Spring Boot applications.
1618

1719
| Time | Session |
1820
|------|---------|
@@ -26,28 +28,21 @@ Goal: Getting started with testing Spring Boot applications and learning how to
2628

2729
### Day Two: The Need for Speed: Optimizing Spring Boot Test Suites
2830

29-
Goal: Showcase and explain strategies to improve build times and speed up tests to gather fast feedback.
30-
31-
| Time | Session |
32-
|------|---------|
33-
| 09:00 - 10:30 | Block 1: [Lab 5](labs/lab-5) - Integration Testing Continued |
34-
| 10:30 - 11:00 | Coffee Break & Exercises |
35-
| 11:00 - 12:30 | Block 2: [Lab 6](labs/lab-6) - Spring Test Context Caching |
36-
| 12:30 - 13:30 | Lunch Break |
37-
| 13:30 - 15:00 | Block 3: [Lab 7](labs/lab-7) - Test Parallelization & Best Practices |
38-
| 15:00 - 15:30 | Coffee Break & Exercises |
39-
| 15:30 - 17:00 | Block 4: [Lab 8](labs/lab-8) - FAQ & CI/CD Best Practices |
40-
41-
## Workshop Format
31+
**Goal**: Showcase and explain strategies to improve build times and speed up tests to gather fast feedback.
4232

43-
- Two-day workshop on-site or remote
44-
- Eight main labs, each 90 minutes
45-
- Hands-on exercises with provided solutions
46-
- Building on a consistent domain model (a library management system)
33+
| Time | Session |
34+
|------|------------------------------------------------------------------------------------------------|
35+
| 09:00 - 10:30 | Block 1: [Lab 5](labs/lab-5) - Integration Testing Continued - Verify the Entire Application |
36+
| 10:30 - 11:00 | Coffee Break & Exercises |
37+
| 11:00 - 12:30 | Block 2: [Lab 6](labs/lab-6) - Understanding Spring TestContext Context Caching for fast Tests |
38+
| 12:30 - 13:30 | Lunch Break |
39+
| 13:30 - 15:00 | Block 3: [Lab 7](labs/lab-7) - Strategies for Fast and Reproducible Spring Boot Test Suites |
40+
| 15:00 - 15:30 | Coffee Break & Exercises |
41+
| 15:30 - 17:00 | Block 4: [Lab 8](labs/lab-8) - General Spring Boot Testing Tips & Tricks and Q&A |
4742

4843
## Lab Structure
4944

50-
Each lab (`lab-1` through `lab-8`) includes:
45+
Each lab in `labs/` (`lab-1` through `lab-8`) includes:
5146

5247
- Exercise files with instructions and TODO comments
5348
- Solution files that show the complete implementation
@@ -63,15 +58,17 @@ Each lab (`lab-1` through `lab-8`) includes:
6358
## Getting Started
6459

6560
1. Clone this repository
66-
6761
2. Import the projects into your IDE of choice.
68-
6962
3. Run all builds with:
7063

7164
```bash
7265
./mvnw verify
7366
```
7467

68+
## Slides
69+
70+
You'll find the slides for each lab in the `slides/` directory. They are organized by lab and can be used as a reference during the workshop.
71+
7572
## Additional Resources
7673

7774
- [Spring Boot Testing Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)
@@ -81,10 +78,3 @@ Each lab (`lab-1` through `lab-8`) includes:
8178
- [Testcontainers Documentation](https://www.testcontainers.org/)
8279
- [WireMock Documentation](http://wiremock.org/docs/)
8380

84-
## Contact
85-
86-
[Contact us](https://pragmatech.digital/contact/) to enroll your team in this workshop.
87-
88-
## License
89-
90-
This project is licensed under the MIT License - see the LICENSE file for details.

labs/lab-5/README.md

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,104 @@
1-
# Lab 4: Integration Testing - Introduction & Strategies
1+
# Lab 5: Full-Stack Integration Testing with MockMvc and WebTestClient
22

33
## Learning Objectives
44

5-
- Learn how to test HTTP clients with WireMock
6-
- Master techniques for testing HTTP-communication-based applications
7-
- Understand the "airplane mode" concept for tests
8-
- Learn to use `@ContextConfiguration` with custom initializers
9-
- Get started with full `@SpringBootTest` integration tests
5+
- Understand the difference between MockMvc (mock web environment) and WebTestClient (real embedded server)
6+
- Write full `@SpringBootTest` integration tests that cover the entire application stack
7+
- Handle authentication and authorization in integration tests
8+
- Manage test data isolation with `@Transactional` and manual `@AfterEach` cleanup
9+
- Use WireMock stubs for external HTTP dependencies within Spring integration tests
1010

11-
## Hints
11+
## Key Concepts
1212

13-
- WireMock provides a flexible API for stubbing HTTP interactions
14-
- Use `@RegisterExtension` for WireMock's JUnit 5 extension
15-
- Sample response JSON files are available in the `__files` directory
16-
- Use `@ContextConfiguration(initializers = ...)` to configure WireMock for Spring integration tests
17-
- All external HTTP calls should be stubbed in tests ("airplane mode")
13+
### MockMvc vs. WebTestClient
14+
15+
| Feature | MockMvc | WebTestClient |
16+
|---|---|---|
17+
| Web environment | `MOCK` (no real server) | `RANDOM_PORT` (real embedded server) |
18+
| Request thread | Same thread as the test | Different server thread |
19+
| `@Transactional` rollback | Works automatically | Does NOT work |
20+
| Authentication | `@WithMockUser` | Real HTTP Basic Auth headers |
21+
| Speed | Faster (no network I/O) | Slightly slower |
22+
23+
### MockMvc + `@Transactional` Pattern
24+
25+
MockMvc dispatches requests through the `DispatcherServlet` in the **same thread** as the test. When `@Transactional` is present on the test class, every test method runs inside a transaction that rolls back automatically at the end — no cleanup code needed.
26+
27+
### WebTestClient + `@AfterEach` Pattern
28+
29+
WebTestClient sends real HTTP requests to the embedded server. Those requests are handled in a **different server thread**, so the server commits its transaction independently. `@Transactional` on the test class has no effect on what the server commits. Manual cleanup (e.g., `bookRepository.deleteAll()`) in `@AfterEach` is required.
1830

1931
## Exercises
2032

21-
### Exercise 1: Testing HTTP Clients with WireMock
33+
### Exercise 1: Integration Testing with MockMvc
2234

23-
In this exercise, you'll create unit tests for the `OpenLibraryApiClient` using WireMock to simulate HTTP interactions.
35+
Write a full integration test using `@SpringBootTest` with the default MOCK web environment.
2436

2537
**Tasks:**
26-
1. Open `Exercise1WireMockTest.java` in the `exercises` package
27-
2. Implement test methods to verify client behavior for:
28-
- Successful API responses (200 status)
29-
- Server error responses (500 status)
30-
3. Optional: Modify the client to handle 404 responses gracefully by returning null instead of throwing an exception
38+
1. Open `Exercise1MockMvcIntegrationTest.java` in the `exercises` package
39+
2. Implement `shouldCreateAndRetrieveBookWhenUsingMockMvc`:
40+
- Inject `OpenLibraryApiStub` and call `stubForSuccessfulBookResponse(isbn)`
41+
- POST to `/api/books` with a valid JSON body and `@WithMockUser(roles = "USER")`
42+
- Assert 201 Created and the presence of a `Location` header
43+
- GET the URL from the `Location` header and assert the returned book fields
44+
3. Implement `shouldReturnAllBooksWhenUsingMockMvc`:
45+
- Pre-populate the database via `BookRepository`
46+
- GET `/api/books` and assert 200 OK with a JSON array
47+
4. Implement `shouldRejectInvalidBookCreationRequest`:
48+
- POST an invalid body (missing fields or wrong ISBN format) and assert 400 Bad Request
3149

3250
**Tips:**
33-
- Use WireMock's JUnit 5 extension (`@RegisterExtension`) or bootstrap it manually
34-
- Configure `WebClient` to point to the WireMock server in your test
35-
- Use WireMock's `stubFor()` method to define response behavior
36-
- Sample response JSON files are available in the `__files` directory (WireMock's default source folder for stubbing)
37-
- Check the solution in `Solution1WireMockTest.java`
51+
- ISBN must be in the format `"978-XXXXXXXXXX"` (dashes required by `BookCreationRequest`)
52+
- `GET /api/books` is public; `GET /api/books/{id}` and `POST /api/books` require `USER` role
53+
- Use `MockMvcRequestBuilders.post(...)` and `MockMvcResultMatchers.jsonPath(...)`
54+
- Inject `WireMockServer` as a bean or use the provided `OpenLibraryApiStub` wrapper
55+
56+
**Observe:** Because `@Transactional` is on the class and MockMvc runs in the same thread, all database changes roll back automatically after each test — no `@AfterEach` cleanup needed.
57+
58+
**File:** `exercises/Exercise1MockMvcIntegrationTest.java`
59+
**Solution:** `solutions/Solution1MockMvcIntegrationTest.java`
60+
61+
---
62+
63+
### Exercise 2: Integration Testing with WebTestClient
64+
65+
Write the same round-trip tests using `@SpringBootTest(webEnvironment = RANDOM_PORT)` and WebTestClient.
66+
67+
**Tasks:**
68+
1. Open `Exercise2WebTestClientIntegrationTest.java` in the `exercises` package
69+
2. Add an `@AfterEach` method that calls `bookRepository.deleteAll()` to clean up committed data
70+
3. Implement `shouldCreateAndRetrieveBookWhenUsingWebTestClient`:
71+
- Stub WireMock for the ISBN
72+
- POST with `.headers(h -> h.setBasicAuth("user", "user"))` and `.bodyValue(requestBody)`
73+
- Extract the `Location` URI via `.returnResult(Void.class).getResponseHeaders().getLocation()`
74+
- GET the location path with basic auth and assert the book fields
75+
4. Implement `shouldReturnAllBooksWhenUsingWebTestClient`:
76+
- Insert books via `BookRepository`, GET `/api/books`, assert a JSON array
77+
5. Implement `shouldRejectUnauthorizedAccessToProtectedEndpoint`:
78+
- GET `/api/books/1` without credentials and assert 401 Unauthorized
79+
80+
**Tips:**
81+
- Use `.headers(h -> h.setBasicAuth("admin", "admin"))` for `ADMIN` role endpoints
82+
- `GET /api/books/{id}` requires `USER` role; without credentials the server returns 401
83+
84+
**Observe:** `@Transactional` on the test class does NOT prevent the server from committing changes. The `@AfterEach` cleanup is essential to keep tests independent.
85+
86+
**File:** `exercises/Exercise2WebTestClientIntegrationTest.java`
87+
**Solution:** `solutions/Solution2WebTestClientIntegrationTest.java`
88+
89+
## How to Run
90+
91+
```bash
92+
# Run all lab-5 tests
93+
./mvnw test -pl labs/lab-5
94+
95+
# Run Exercise 1
96+
./mvnw test -pl labs/lab-5 -Dtest="Exercise1MockMvcIntegrationTest"
97+
98+
# Run Exercise 2
99+
./mvnw test -pl labs/lab-5 -Dtest="Exercise2WebTestClientIntegrationTest"
100+
101+
# Run solutions
102+
./mvnw test -pl labs/lab-5 -Dtest="Solution1MockMvcIntegrationTest"
103+
./mvnw test -pl labs/lab-5 -Dtest="Solution2WebTestClientIntegrationTest"
104+
```

labs/lab-6/README.md

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
- Understand how Spring's TestContext caching mechanism works
66
- Identify common configuration mistakes that break context caching
77
- Measure the performance impact of `@DirtiesContext` and `@MockitoBean`
8-
- Apply the SharedIntegrationTestBase pattern to maximize context reuse
8+
- Apply the `SharedIntegrationTestBase` pattern to maximize context reuse
99
- Reduce integration test suite execution time
1010

1111
## How Context Caching Works
1212

13-
Spring's TestContext framework caches application contexts between test classes to avoid the expensive cost of starting a new context for every test. A single Spring Boot context startup typically takes 5-10 seconds, so a test suite with 20 integration test classes could take 100-200 seconds just on context initialization if caching is broken.
13+
Spring's TestContext framework caches application contexts between test classes to avoid the expensive cost of starting a new context for every test. A single Spring Boot context startup typically takes 510 seconds, so a test suite with 20 integration test classes could take 100200 seconds just on context initialization if caching is broken.
1414

1515
The context cache key is composed of:
1616

@@ -34,13 +34,13 @@ If two test classes have the **exact same** combination of these attributes, the
3434
| `@Transactional` | No | Only affects transaction behavior, not the context key |
3535
| Different test method names | No | Methods are not part of the cache key |
3636

37-
## The @DirtiesContext Problem
37+
## The `@DirtiesContext` Problem
3838

3939
`@DirtiesContext` is the most expensive annotation in terms of test performance. It marks the context as "dirty" and forces Spring to destroy it and create a new one:
4040

4141
```java
42-
// AVOID: Destroys and recreates context after EVERY test method
43-
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
42+
// AVOID: Destroys and recreates context before EVERY test method
43+
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
4444

4545
// AVOID: Destroys context after the entire test class
4646
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
@@ -52,16 +52,16 @@ If two test classes have the **exact same** combination of these attributes, the
5252
- Reset WireMock stubs in `@AfterEach` methods
5353
- Design beans to be stateless
5454

55-
## The SharedIntegrationTestBase Pattern
55+
## The `SharedIntegrationTestBase` Pattern
5656

57-
The recommended approach is to create a single abstract base class with all common test annotations:
57+
The recommended approach is a single abstract base class with all common test annotations:
5858

5959
```java
6060
@SpringBootTest
6161
@Import(LocalDevTestcontainerConfig.class)
6262
@ContextConfiguration(initializers = WireMockContextInitializer.class)
6363
public abstract class SharedIntegrationTestBase {
64-
// Common test infrastructure
64+
// Common test infrastructure provided via annotations above
6565
}
6666
```
6767

@@ -70,11 +70,11 @@ All integration tests extend this base class:
7070
```java
7171
class MyFeatureIT extends SharedIntegrationTestBase {
7272
@Autowired
73-
private MyService myService;
73+
private BookRepository bookRepository;
7474

7575
@Test
7676
void shouldDoSomething() {
77-
// Uses the shared context - no extra startup cost
77+
// Uses the shared context no extra startup cost
7878
}
7979
}
8080
```
@@ -85,49 +85,71 @@ class MyFeatureIT extends SharedIntegrationTestBase {
8585
3. Do NOT add `@TestPropertySource` with unique properties
8686
4. Do NOT add `@ActiveProfiles` with a different profile
8787
5. Use WireMock stubs for external API simulation instead of mocking clients
88-
6. Use `@Transactional` or `@Sql` for data isolation (these do not break caching)
88+
6. Use `@Transactional` or `@Sql` for data isolation these do not break caching
8989

9090
## Exercises
9191

9292
### Exercise 1: Context Caching Analysis
9393

94-
Run the five `ContextCacheKiller*IT` tests in the `experiment` package and analyze:
95-
1. How many application contexts are created (look for "Initializing Spring" log messages)
96-
2. What configuration difference in each test causes a separate context
97-
3. Propose changes to reduce the number of contexts to ONE
94+
Observe which test configurations break Spring's context cache and document your findings.
95+
96+
**Tasks:**
97+
1. Open `Exercise1ContextCachingAnalysis.java` in the `exercises` package
98+
2. Run the five `ContextCacheKiller*IT` tests and count the application contexts created
99+
- Look for `"Initializing Spring"` log messages in the console output
100+
3. For each of the five tests, identify what configuration difference causes a separate context
101+
4. Fill in the analysis comments in `Exercise1ContextCachingAnalysis.java`
102+
5. Propose the minimal changes to reduce all five tests to share ONE context
103+
104+
**Run:**
105+
```bash
106+
./mvnw test -pl labs/lab-6 -Dtest="ContextCacheKiller*IT"
107+
```
108+
109+
**File:** `exercises/Exercise1ContextCachingAnalysis.java`
110+
**Solution:** `solutions/Solution1ContextCachingAnalysis.java`
111+
112+
---
98113

99114
### Exercise 2: Shared Base Class
100115

101-
Create a `SharedIntegrationTestBase` and refactor all integration tests to extend it, ensuring only a single application context is created across the entire test suite.
116+
Apply the `SharedIntegrationTestBase` pattern to eliminate duplicate context creation across the suite.
117+
118+
**Tasks:**
119+
1. Open `Exercise2SharedBaseClassTest.java` — make the class extend `SharedIntegrationTestBase` (already provided in the `config` package)
120+
2. Add an `@Autowired BookRepository` field and assert it is not null in the test
121+
3. Refactor the `ContextCacheKiller*IT` tests to extend `SharedIntegrationTestBase`:
122+
- Remove duplicate `@SpringBootTest`, `@Import`, `@ContextConfiguration` from each test
123+
- Remove `@DirtiesContext` — it defeats caching entirely
124+
- Remove `@MockitoBean` annotations — use WireMock stubs from the shared context instead
125+
- Remove `@TestPropertySource` with unique values — move shared properties to `application-test.yml`
126+
- Remove `@ActiveProfiles` differences — keep profiles consistent across the suite
127+
4. Run all integration tests and verify only ONE `"Initializing Spring"` log line appears
128+
5. Compare build time before and after the optimization
129+
130+
**File:** `exercises/Exercise2SharedBaseClassTest.java`
131+
**Solution:** `solutions/Solution2SharedBaseClassTest.java`
102132

103133
## Hints
104134

105135
- Look for `"Initializing Spring"` in the console output to count context creations
106-
- The experiment files in `src/test/java/.../experiment/` demonstrate both bad and good patterns
107-
- `DirtiesContextDemoIT` shows the per-method cost of `@DirtiesContext`
108-
- `OptimizedContextReuseIT` shows the ideal pattern using `SharedIntegrationTestBase`
136+
- `SharedIntegrationTestBase` is already implemented in the `config` package — extend it, don't recreate it
109137
- Solutions are available in the `solutions` package
110138

111139
## How to Run
112140

113141
```bash
114-
# Run only the ContextCacheKiller experiments (observe multiple contexts)
142+
# Run only the ContextCacheKiller tests (observe multiple contexts)
115143
./mvnw test -pl labs/lab-6 -Dtest="ContextCacheKiller*IT"
116144

117-
# Run the DirtiesContext demo (observe context reloads)
118-
./mvnw test -pl labs/lab-6 -Dtest="DirtiesContextDemoIT"
119-
120-
# Run the optimized test (observe context reuse)
121-
./mvnw test -pl labs/lab-6 -Dtest="OptimizedContextReuseIT"
122-
123145
# Run all lab-6 tests
124146
./mvnw test -pl labs/lab-6
125147
```
126148

127149
## Key Takeaways
128150

129-
1. **Context caching is automatic** but fragile - small annotation differences break it
130-
2. **@DirtiesContext is expensive** - avoid it unless absolutely necessary
131-
3. **@MockitoBean creates new contexts** - prefer WireMock stubs for HTTP clients
132-
4. **A shared base class** is the simplest way to ensure consistent annotations
151+
1. **Context caching is automatic** but fragile small annotation differences break it
152+
2. **`@DirtiesContext` is expensive** avoid it unless absolutely necessary
153+
3. **`@MockitoBean` creates new contexts** prefer WireMock stubs for HTTP client simulation
154+
4. **A shared base class** is the simplest way to ensure consistent annotations across all integration tests
133155
5. **Measure your build time** before and after optimizing context caching

0 commit comments

Comments
 (0)