diff --git a/labs/lab-1/pom.xml b/labs/lab-1/pom.xml
index 04bd22f..a2a0fc3 100644
--- a/labs/lab-1/pom.xml
+++ b/labs/lab-1/pom.xml
@@ -16,7 +16,6 @@
21
- 5.5.0
diff --git a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/experiment/MockitoDemoTest.java b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/experiment/MockitoDemoTest.java
index 09aecb3..2fe046d 100644
--- a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/experiment/MockitoDemoTest.java
+++ b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/experiment/MockitoDemoTest.java
@@ -7,6 +7,7 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import pragmatech.digital.workshops.lab1.domain.Book;
import pragmatech.digital.workshops.lab1.repository.BookRepository;
@@ -23,11 +24,13 @@ class MockitoDemoTest {
@Mock
private BookRepository bookRepository;
+ // public int count() -> int -> 0
+ // public Integer count() -> null
+
@InjectMocks
private BookService bookService;
@Test
- @Disabled("Disabled to prevent test failure due to Sunday restriction in BookService")
void shouldReturnBookWhenFound() {
// Arrange
when(bookRepository.findByIsbn("1234")).thenReturn(Optional.empty());
diff --git a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/BookServiceWithClock.java b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/BookServiceWithClock.java
index 730c1b7..d14c01e 100644
--- a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/BookServiceWithClock.java
+++ b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/BookServiceWithClock.java
@@ -5,6 +5,8 @@
import java.time.LocalDate;
import java.util.Optional;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
import pragmatech.digital.workshops.lab1.domain.Book;
import pragmatech.digital.workshops.lab1.repository.BookRepository;
import pragmatech.digital.workshops.lab1.service.BookAlreadyExistsException;
diff --git a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/RefactoredBookService.java b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/RefactoredBookService.java
index fa65e00..3de4d15 100644
--- a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/RefactoredBookService.java
+++ b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/RefactoredBookService.java
@@ -8,6 +8,7 @@
import pragmatech.digital.workshops.lab1.service.BookAlreadyExistsException;
import pragmatech.digital.workshops.lab1.util.TimeProvider;
+import static java.time.DayOfWeek.SATURDAY;
import static java.time.DayOfWeek.SUNDAY;
public class RefactoredBookService {
diff --git a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/Solution1UnitTest.java b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/Solution1UnitTest.java
index 87f3fae..20c0f26 100644
--- a/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/Solution1UnitTest.java
+++ b/labs/lab-1/src/test/java/pragmatech/digital/workshops/lab1/solutions/Solution1UnitTest.java
@@ -6,7 +6,6 @@
import java.time.ZoneId;
import java.util.Optional;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,7 +24,6 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static tools.jackson.databind.ext.javatime.deser.JSR310StringParsableDeserializer.ZONE_ID;
@ExtendWith(MockitoExtension.class)
class Solution1UnitTest {
@@ -53,7 +51,6 @@ void shouldThrowExceptionWhenBookWithIsbnAlreadyExists() {
@Test
@DisplayName("Should create a book when ISBN does not exist")
- @Disabled("Disabled to prevent test failure due to Sunday restriction in BookService")
void shouldCreateBookWhenIsbnDoesNotExist() {
// Arrange
BookService cut = new BookService(bookRepository);
@@ -87,7 +84,8 @@ void shouldCreateBookWhenIsbnDoesNotExist() {
void shouldThrowExceptionWhenTryingToRegisterBookOnSunday() {
// Arrange
TimeProvider timeProvider = mock(TimeProvider.class);
- when(timeProvider.getCurrentDate()).thenReturn(LocalDate.of(2026, 3, 1)); // A Sunday
+ when(timeProvider.getCurrentDate())
+ .thenReturn(LocalDate.of(2026, 3, 1)); // A Sunday
RefactoredBookService cut = new RefactoredBookService(bookRepository, timeProvider);
String isbn = "9780134685991";
diff --git a/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/experiment/BookControllerTest.java b/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/experiment/BookControllerTest.java
index df5c100..fcb5d1f 100644
--- a/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/experiment/BookControllerTest.java
+++ b/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/experiment/BookControllerTest.java
@@ -114,20 +114,20 @@ void shouldReturnNotFoundWhenBookDoesNotExistById() throws Exception {
@Test
void shouldReturnCreatedWithLocationWhenCreatingBookWithValidData() throws Exception {
// Arrange
- BookCreationRequest request = new BookCreationRequest(
- "123-1234567890",
- "Test Book",
- "Test Author",
- LocalDate.of(2020, 1, 1)
- );
-
when(bookService.createBook(any(BookCreationRequest.class))).thenReturn(1L);
// Act & Assert
mockMvc.perform(post("/api/books")
.with(SecurityMockMvcRequestPostProcessors.user("user").roles("USER"))
.contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(request)))
+ .content("""
+ {
+ "isbn": "123-1234567890",
+ "title": "Test Book",
+ "author": "Test Author",
+ "publishedDate": "2024-01-01"
+ }
+ """))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/books/1")));
}
diff --git a/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/solutions/Solution1WebMvcTest.java b/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/solutions/Solution1WebMvcTest.java
index 510eb76..b93c314 100644
--- a/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/solutions/Solution1WebMvcTest.java
+++ b/labs/lab-2/src/test/java/pragmatech/digital/workshops/lab2/solutions/Solution1WebMvcTest.java
@@ -50,15 +50,13 @@ void shouldReturnUnauthorizedWhenNoAuthenticationIsProvided() throws Exception {
// Arrange - No authentication credentials provided
// Act & Assert
- mockMvc.perform(delete("/api/books/1"))
+ mockMvc
+ .perform(delete("/api/books/1"))
.andExpect(status().isUnauthorized());
-
- // Verify - Service should not be called
- verify(bookService, times(0)).deleteBook(any());
}
@Test
- @WithMockUser(roles = "USER")
+ @WithMockUser(roles = "DUKE")
@DisplayName("Should return 403 Forbidden when authenticated with insufficient privileges")
void shouldReturnForbiddenWhenAuthenticatedWithInsufficientPrivileges() throws Exception {
// Arrange - User with insufficient privileges (USER role)
@@ -66,9 +64,6 @@ void shouldReturnForbiddenWhenAuthenticatedWithInsufficientPrivileges() throws E
// Act & Assert
mockMvc.perform(delete("/api/books/1"))
.andExpect(status().isForbidden());
-
- // Verify - Service should not be called
- verify(bookService, times(0)).deleteBook(any());
}
@Test
@@ -81,9 +76,6 @@ void shouldReturnNoContentWhenAuthenticatedAsAdminAndBookExists() throws Excepti
// Act & Assert
mockMvc.perform(delete("/api/books/1"))
.andExpect(status().isNoContent());
-
- // Verify - Service should be called with the book ID
- verify(bookService, times(1)).deleteBook(1L);
}
@Test
@@ -98,7 +90,7 @@ void shouldReturnNotFoundWhenAuthenticatedAsAdminButBookDoesntExist() throws Exc
.andExpect(status().isNotFound());
// Verify - Service should be called with the book ID
- verify(bookService, times(1)).deleteBook(999L);
+ //verify(bookService, times(1)).deleteBook(999L);
}
}
@@ -129,9 +121,6 @@ void shouldReturnCreatedWhenValidBookDataIsProvided() throws Exception {
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(header().string("Location", Matchers.containsString("/api/books/1")));
-
- // Verify - Service should be called
- verify(bookService, times(1)).createBook(any());
}
@Test
@@ -142,7 +131,7 @@ void shouldReturnBadRequestWhenInvalidBookDataIsProvided() throws Exception {
String invalidBookJson = """
{
"isbn": "",
- "title": "",
+ "title": "Test",
"author": "Test Author",
"publishedDate": "2025-01-01"
}
diff --git a/labs/lab-3/src/main/java/pragmatech/digital/workshops/lab3/Lab3Application.java b/labs/lab-3/src/main/java/pragmatech/digital/workshops/lab3/Lab3Application.java
index 65fe314..e815ba6 100644
--- a/labs/lab-3/src/main/java/pragmatech/digital/workshops/lab3/Lab3Application.java
+++ b/labs/lab-3/src/main/java/pragmatech/digital/workshops/lab3/Lab3Application.java
@@ -4,7 +4,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
-// @EnableJpaAuditing -> keep this class clean and outsource configuration to a separate class
+//@EnableJpaAuditing
@SpringBootApplication
public class Lab3Application {
diff --git a/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/experiment/BookRepositoryTest.java b/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/experiment/BookRepositoryTest.java
index f46017b..5d49c97 100644
--- a/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/experiment/BookRepositoryTest.java
+++ b/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/experiment/BookRepositoryTest.java
@@ -32,9 +32,6 @@ class BookRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
- .withDatabaseName("testdb")
- .withUsername("test")
- .withPassword("test")
.withInitScript("init-postgres.sql");
@Autowired
diff --git a/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/solutions/Solution1DataJpaTest.java b/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/solutions/Solution1DataJpaTest.java
index 95fb402..c3dc737 100644
--- a/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/solutions/Solution1DataJpaTest.java
+++ b/labs/lab-3/src/test/java/pragmatech/digital/workshops/lab3/solutions/Solution1DataJpaTest.java
@@ -4,17 +4,15 @@
import java.util.List;
import java.util.Optional;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
-import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
-import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.postgresql.PostgreSQLContainer;
import pragmatech.digital.workshops.lab3.entity.Book;
import pragmatech.digital.workshops.lab3.entity.BookStatus;
import pragmatech.digital.workshops.lab3.repository.BookRepository;
@@ -30,15 +28,11 @@
*/
@DataJpaTest
@Testcontainers
-@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class Solution1DataJpaTest {
@Container
@ServiceConnection
- static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
- .withDatabaseName("testdb")
- .withUsername("test")
- .withPassword("test")
+ static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:16-alpine")
.withInitScript("init-postgres.sql");
@Autowired
diff --git a/labs/lab-3/src/test/resources/application.yml b/labs/lab-3/src/test/resources/application.yml
index 805acc4..86ecfaa 100644
--- a/labs/lab-3/src/test/resources/application.yml
+++ b/labs/lab-3/src/test/resources/application.yml
@@ -1,11 +1,10 @@
spring:
- datasource:
- url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
- username: sa
- password: password
- driver-class-name: org.h2.Driver
jpa:
+ hibernate:
+ ddl-auto: none
properties:
hibernate:
-# format_sql: true
-# highlight_sql: true
+ format_sql: true
+ highlight_sql: true
+
+
diff --git a/labs/lab-7/README.md b/labs/lab-7/README.md
index a39ea51..157de20 100644
--- a/labs/lab-7/README.md
+++ b/labs/lab-7/README.md
@@ -2,16 +2,16 @@
## Learning Objectives
-- Configure JUnit 5 parallel test execution via `junit-platform.properties`
-- Understand the difference between JUnit 5 thread-level parallelism and Maven Surefire `forkCount` process-level forking
+- Configure JUnit 6 parallel test execution via `junit-platform.properties`
+- Understand the difference between JUnit 6 thread-level parallelism and Maven Surefire `forkCount` process-level forking
- Identify and fix test isolation issues caused by shared database state
- Apply `@Transactional` and UUID-based unique data as complementary isolation strategies
## Key Concepts
-### JUnit 5 Parallel Execution
+### JUnit 6 Parallel Execution
-JUnit 5 supports running tests in parallel at both the class level and the method level. Configuration lives in `src/test/resources/junit-platform.properties`.
+JUnit 6 supports running tests in parallel at both the class level and the method level. Configuration lives in `src/test/resources/junit-platform.properties`.
There are two independent axes of parallelism:
@@ -71,12 +71,12 @@ src/test/resources/
### Exercise 1: Configure and Observe Parallel Test Execution
-Understand JUnit 5 parallel execution configuration and observe its effect on thread allocation.
+Understand JUnit 6 parallel execution configuration and observe its effect on thread allocation.
**Tasks:**
1. Open `src/test/resources/junit-platform.properties` and review the current settings
2. Open `Exercise1ParallelExecutionTest.java` — the test methods print the current thread name
-3. Run `mvn test` and observe which threads each test class runs on in the console output
+3. Run `mvn test` or within IntelliJ and observe which threads each test class runs on in the console output
4. Try different parallelism strategies by modifying `junit-platform.properties`:
- Classes concurrent, methods `same_thread` (current default)
- Both classes and methods `concurrent`
diff --git a/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise1ParallelExecutionTest.java b/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise1ParallelExecutionTest.java
index b23baf3..16deebb 100644
--- a/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise1ParallelExecutionTest.java
+++ b/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise1ParallelExecutionTest.java
@@ -9,6 +9,7 @@
*
* - Check the junit-platform.properties file in src/test/resources
* - Run all tests and observe parallel execution in the log output
+ * - Consult JUnit Docs
* - Experiment with different parallelism strategies:
*
* - a) Classes concurrent, methods same_thread (default - current setting)
diff --git a/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise2TestIsolationTest.java b/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise2TestIsolationTest.java
index ad60bdb..a339f3e 100644
--- a/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise2TestIsolationTest.java
+++ b/labs/lab-7/src/test/java/pragmatech/digital/workshops/lab7/exercises/Exercise2TestIsolationTest.java
@@ -19,7 +19,6 @@
*
* Tasks:
*
- * - Identify tests that fail when run in parallel (shared database state)
* - Apply isolation strategies:
*
* - a) Use @Transactional for automatic rollback after each test
diff --git a/slides/assets/ai-image.jpg b/slides/assets/ai-image.jpg
new file mode 100644
index 0000000..5a10251
Binary files /dev/null and b/slides/assets/ai-image.jpg differ
diff --git a/slides/assets/newsletter-signup-qr.png b/slides/assets/newsletter-signup-qr.png
new file mode 100644
index 0000000..a236d31
Binary files /dev/null and b/slides/assets/newsletter-signup-qr.png differ
diff --git a/slides/assets/tsbad-cover.png b/slides/assets/tsbad-cover.png
new file mode 100644
index 0000000..402b857
Binary files /dev/null and b/slides/assets/tsbad-cover.png differ
diff --git a/slides/assets/why-test-software.jpg b/slides/assets/why-test-software.jpg
new file mode 100644
index 0000000..98dc5fe
Binary files /dev/null and b/slides/assets/why-test-software.jpg differ
diff --git a/slides/lab-1.md b/slides/lab-1.md
index 915bac6..32d1f77 100644
--- a/slides/lab-1.md
+++ b/slides/lab-1.md
@@ -39,9 +39,7 @@ Philip Riecks - [PragmaTech GmbH](https://pragmatech.digital/) - [@rieckpil](htt
# Organization
-- Hotel WiFi: `TBD` Password: `TBD`
- Slides & Code will be shared
-- Sharing links during the workshop:
- Please interrupt me any time if you have questions or want to share your experience
- Workshop lab requirements
- Java 21
@@ -156,6 +154,28 @@ Notes:
---
+
+
+
+# Why Test Software?
+
+---
+
+
+
+
+
+## Automated Testing in the AI Era
+
+- AI generates the code; you own the consequences.
+- Co-pilots don’t carry pagers. You do.
+- The faster the code is generated, the faster you need to prove it’s correct.
+- AI is the accelerator; your test suite is the brakes. You can't drive fast without both.
+- Mass-produced code without (the correct) mass-produced tests is just technical debt at scale.
+
+---
+
+
### Common Testing Misconceptions
- Testing is only for finding bugs
@@ -232,7 +252,28 @@ Good tests don't just catch bugs - they give you **fast feedback** and **confide
---
+## Maven Coordinates
+```xml
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+```
+
+---
@@ -740,7 +781,7 @@ public class TimingExtension implements BeforeTestExecutionCallback, AfterTestEx
- IDE of your choice (I can support you with IntelliJ IDEA)
- Java 21
- Docker Engine configured to run with Testcontainers
-- Work locally or use GitHub Codespaces (120 hours/month free) as a fallback if you have trouble setting up your local environment
+- Work locally or use GitHub Codespaces (120 hours/month free) as a fallback if you have trouble setting up your local environment (don't forget to remove the Codespace after the workshop to avoid running out of hours)
- Fore Codespaces, pick at least 4-Cores (16 GB RAM) and region `Europe West`
diff --git a/slides/lab-3.md b/slides/lab-3.md
index 49a9cfa..0c900cc 100644
--- a/slides/lab-3.md
+++ b/slides/lab-3.md
@@ -21,6 +21,23 @@ Philip Riecks - [PragmaTech GmbH](https://pragmatech.digital/) - [@rieckpil](htt
---
+### Updated Timeline for the First Workshop Day
+
+Completed:
+
+- 9:00 - 10:30: **Testing Basics and Unit Testing with Spring Boot**
+- 10:30 - 11:00: **Coffee Break & Time for Exercises**
+- 11:00 - 12:30: **Sliced Testing - Introduction and Verifying the Web Layer**
+- 12:30 - 13:30: **Lunch & Time for Exercises**
+
+Next:
+
+- 13:30 - 15:00: **Sliced Testing Continued, including Testing the Persistence Layer**
+- 15:00 - 15:30: **Coffee Break & Time for Exercises**
+- **15:30 - 16:30:** (Updated) **Integration Testing - Introduction and Strategies to Start the Entire Context**
+
+---
+
## Discuss Exercises from Lab 2
- Exercise 1: `@WebMvcTest`
diff --git a/slides/lab-4.md b/slides/lab-4.md
index c780c37..8ee6e98 100644
--- a/slides/lab-4.md
+++ b/slides/lab-4.md
@@ -402,4 +402,4 @@ class SampleIT {
## Day 2 starts at 09:00
-_No lab exercise for Lab 4 - content feeds directly into tomorrow's exercises_
+_Optional exercise for lab 4: Learn how to use WireMock with `Exercise1WireMockTest`_
diff --git a/slides/lab-5.md b/slides/lab-5.md
index b657243..bbd293d 100644
--- a/slides/lab-5.md
+++ b/slides/lab-5.md
@@ -46,10 +46,10 @@ Philip Riecks - [PragmaTech GmbH](https://pragmatech.digital/) - [@rieckpil](htt
- 9:00 - 10:30: **Integration Testing Continued - Verify the Entire Application**
- 10:30 - 11:00: **Coffee Break & Time for Exercises**
- 11:00 - 12:30: **Understanding Spring TestContext Context Caching for fast Tests**
-- 12:30 - 13:30: **Lunch**
+- 12:30 - 13:30: **Lunch & Time for Exercises**
- 13:30 - 15:00: **Strategies for Fast and Reproducible Spring Boot Test Suites**
- 15:00 - 15:30: **Coffee Break & Time for Exercises**
-- 15:30 - 17:00: **General Spring Boot Testing Tips & Tricks and Q&A**
+- 15:30 - 16:30: **General Spring Boot Testing Tips & Tricks and Q&A**
Each 90-minute lab session will include a mix of explanations, demonstrations, and hands-on exercises.
@@ -461,4 +461,5 @@ pragmatech.digital.workshops.lab5.experiment.customizer.SharedInfrastructureCust
- Navigate to the `labs/lab-5` folder and complete the tasks in the `README`
- **Exercise 1**: Implement an integration test with `MockMvc` (MOCK environment, `@Transactional`)
- **Exercise 2**: Implement the same scenario with `WebTestClient` (RANDOM_PORT, manual cleanup)
+- **Exercise 3** (Optional): Implement the exercise from lab 4 to learn how to use WireMock
- Time boxed: until the end of the coffee break
diff --git a/slides/lab-7.md b/slides/lab-7.md
index 0de574d..7f6c575 100644
--- a/slides/lab-7.md
+++ b/slides/lab-7.md
@@ -45,7 +45,7 @@ Philip Riecks - [PragmaTech GmbH](https://pragmatech.digital/) - [@rieckpil](htt
**Goal**: Reduce build time by running tests concurrently
-Two independent mechanisms — they work at different levels:
+Two independent mechanisms - they work at different levels:
| Mechanism | Level | Isolation |
|---|---|---|
@@ -70,9 +70,9 @@ Splits tests across multiple **separate JVM processes**:
```
-- `forkCount=1` — default: one JVM for all tests
-- `forkCount=2` — two JVMs running in parallel
-- `forkCount=1C` — one JVM per available CPU core (dynamic)
+- `forkCount=1` - default: one JVM for all tests
+- `forkCount=2` - two JVMs running in parallel
+- `forkCount=1C` - one JVM per available CPU core (dynamic)
> **Maven Failsafe** works the same way for `*IT.java` integration tests.
@@ -266,9 +266,7 @@ class BookIT {
---
-## Optimize Containers Time
-
-### Correct Usage of Testcontainers
+# Correct Usage of Testcontainers
---
@@ -402,9 +400,7 @@ testcontainers.reuse.enable=true
## Tip 1: Hide Test Output, Show on Failure
-By default, all test output floods the console.
-
-Redirect it to files:
+By default, all test output floods the console. We can redirect it to files:
```xml
@@ -417,6 +413,14 @@ Redirect it to files:
```
+```yaml
+# as a last step on GitHub actions
+- name: Log test output on failure
+ if: failure() || cancelled()
+ run: find . -type f -path "*test-reports/*-output.txt" -exec tail -n +1 {} +
+
+```
+
---
## Tip 2: Rerun Flaky Tests Automatically
@@ -533,6 +537,90 @@ Separate CI-specific settings from the default developer experience:
---
+## GHA Workflow Design: Start Pragmatic
+
+One file, one workflow - perfectly fine for most projects:
+
+```yaml
+# .github/workflows/build.yml
+name: Build
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with: { java-version: '21', distribution: 'temurin', cache: maven }
+ - run: ./mvnw verify
+```
+
+**Start here.** Measure total build time. Only split when you have a concrete reason (too slow, unrelated failure domains, different schedules).
+
+---
+
+## GHA Workflow Design: When to Split
+
+Once the single workflow grows unwieldy, extract by **concern and cadence**:
+
+```text
+.github/workflows/
+ build.yml ← on: push/PR — compile + unit + integration tests
+ coding-standards.yml ← on: push/PR — Checkstyle, SpotBugs, ArchUnit
+ nightly.yml ← on: schedule — slow E2E, mutation tests, full matrix
+ deploy.yml ← on: push to main — build image + deploy to staging
+```
+
+Separate workflows run **in parallel** automatically - no extra config needed.
+
+---
+
+## Off-topic: pre-commit — Catch Issues Before You Push
+
+[pre-commit](https://pre-commit.com) runs checks automatically on `git commit`, before code ever reaches CI:
+
+```yaml
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ hooks:
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: https://github.com/ejba/pre-commit-maven
+ hooks:
+ - id: maven
+ args: ['-f pom.xml spotless:apply']
+```
+
+Style violations and formatting errors fail **locally in milliseconds** - not 5 minutes into a CI run. Less noise in PRs, faster feedback loop.
+
+
+---
+
+## Best Practice 0: Never Go Stale
+
+**Deploy or run at least once a week** - even if nobody pushed code.
+
+```yaml
+# .github/workflows/nightly.yml
+name: Nightly Verification
+on:
+ schedule:
+ - cron: '0 3 * * 1' # Every Monday at 03:00 UTC
+ workflow_dispatch: # Allow manual trigger
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+```
+
+Stale pipelines break silently: secrets expire, base images get security patches, dependency resolution drifts. A weekly run catches this **before** it blocks a real release.
+
+---
+
+
## Best Practice 1: Always Set a Job Timeout
Hanging Testcontainers or infinite loops block CI slots for hours without a timeout:
diff --git a/slides/lab-8.md b/slides/lab-8.md
index f76ce9f..6958934 100644
--- a/slides/lab-8.md
+++ b/slides/lab-8.md
@@ -82,7 +82,7 @@ class OutputCaptureTest {
---
-# Mutation Testing with PIT
+## Mutation Testing with PIT
**The problem:** 100% line coverage ≠ good tests. Weak assertions can still pass.
@@ -99,15 +99,13 @@ void shouldReturnFee() {
```bash
cd labs/lab-8
./mvnw pitest:mutationCoverage
-# Opens target/pit-reports/index.html
+# Open target/pit-reports/index.html
```
-> **A mutation is "killed"** when at least one test fails because of the mutation.
-> **A surviving mutant** exposes a gap in your test suite.
---
-## PIT in Action — `LateReturnFeeCalculator`
+## PIT in Action - `LateReturnFeeCalculator`
```java
// Production code with multiple fee tiers
@@ -125,7 +123,8 @@ public BigDecimal calculateFee(Book book, LocalDate borrowedDate) {
**PIT mutates conditionals** (`<= 7` → `< 7`, `<= 14` → `< 14`) — boundary tests are essential.
-**Pro tip:** Use `Clock` injection instead of `LocalDate.now()` for deterministic tests.
+> **A mutation is "killed"** when at least one test fails because of the mutation.
+> **A surviving mutant** exposes a gap in your test suite.
---
@@ -194,7 +193,7 @@ class RecordApplicationEventsTest {
## `ApplicationContextRunner`
-Test **auto-configuration and conditional beans** without starting a full Spring Boot context.
+Test **auto-configuration and conditional beans** without starting a full context.
```java
class ApplicationContextRunnerTest {
@@ -202,13 +201,6 @@ class ApplicationContextRunnerTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(ConditionalBookImportConfig.class); // ← Minimal slice
- @Test
- void shouldNotHaveBookImportBeanWhenPropertyIsAbsent() {
- contextRunner.run(context ->
- assertThat(context).doesNotHaveBean("bookImportEnabled")
- );
- }
-
@Test
void shouldHaveBookImportBeanWhenPropertyIsEnabled() {
contextRunner
@@ -220,7 +212,7 @@ class ApplicationContextRunnerTest {
}
```
-**Runs in milliseconds** — ideal for testing `@ConditionalOnProperty`, `@ConditionalOnClass`, `@ConditionalOnMissingBean`.
+**Runs in milliseconds** - ideal for testing `@ConditionalOnProperty`, `@ConditionalOnClass`, `@ConditionalOnMissingBean`.
---
@@ -253,7 +245,7 @@ class ApplicationContextRunnerTest {
---
-## ArchUnit — Code Examples
+## ArchUnit - Code Examples
```java
@AnalyzeClasses(
@@ -535,7 +527,7 @@ log.info("Book created");
**Good observability checklist:**
- Structured JSON logs (Logback + `logstash-logback-encoder`) shipped to a central store
- Dashboards scoped to **error rate**, **p99 latency**, and **business KPIs** - not CPU graphs
-- Runbooks linked directly from alert notifications
+- [Runbooks](https://github.com/stratospheric-dev/stratospheric/tree/main/docs/runbooks) linked directly from alert notifications
---
@@ -556,13 +548,6 @@ commit → CI (tests pass) → build image → push to registry
---
-**Rollback triggers to Monitor**
-- Health check endpoint (`/actuator/health`) returns non-200 for > 60 s after deploy
-- Error rate spikes above baseline within the first 5 minutes
-- P99 latency exceeds SLO threshold post-deploy
-
----
-
## Blue/Green & A/B Deployments
**Blue/green** eliminates downtime and provides instant rollback: run two identical environments, flip the load balancer, keep the old environment warm.
@@ -600,7 +585,9 @@ Requires feature-flag or traffic-splitting infrastructure (Nginx, Istio, AWS ALB
## Feature Flags: Decouple Deployment from Release
**Deployment** = shipping code to production.
-**Release** = making a feature visible to users. Feature flags let you do both independently.
+**Release** = making a feature visible to users.
+
+Feature flags let you do both independently.
```java
if (featureFlags.isEnabled("new-book-search", userId)) {
@@ -728,6 +715,23 @@ The goal is not zero failures - it is **fast, automatic recovery** when failures
---
+## Don't Leave Empty-Handed
+
+
+
+
+- Get the complementary **Testing Spring Boot Applications Demystified** for free
+
+- 120+ Pages with practical hands-on advice to ship code with confidence
+
+- Get the eBook by joining our [newsletter](https://rieckpil.de/free-spring-boot-testing-book/) and receive further Spring Boot testing-related tips & tricks
+
+
+
+---
+
+
+
## Joyful Testing!
Workshop materials are on [GitHub](https://github.com/PragmaTech-GmbH/digdir-workshop/).