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 @@ *
    *
  1. Check the junit-platform.properties file in src/test/resources
  2. *
  3. Run all tests and observe parallel execution in the log output
  4. + *
  5. Consult JUnit Docs
  6. *
  7. 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: *

        - *
      1. Identify tests that fail when run in parallel (shared database state)
      2. *
      3. 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: --- + +![bg right:33%](assets/why-test-software.jpg) + +# Why Test Software? + +--- + + +![bg right:33%](assets/ai-image.jpg) + + +## 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 + +![bg h:720 w:450 right:33%](assets/tsbad-cover.png) + + +- 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 + +![center h:200 w:200](assets/newsletter-signup-qr.png) + +--- + + + ## Joyful Testing! Workshop materials are on [GitHub](https://github.com/PragmaTech-GmbH/digdir-workshop/).