Skip to content

Commit 2cbad04

Browse files
committed
next steps
1 parent 03daff5 commit 2cbad04

4 files changed

Lines changed: 131 additions & 66 deletions

File tree

labs/lab-8/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@
179179
<param>pragmatech.digital.workshops.lab8.service.LateReturnFeeCalculator</param>
180180
</targetClasses>
181181
<targetTests>
182-
<param>pragmatech.digital.workshops.lab8.experiment.LateReturnFeeCalculatorTest</param>
182+
<!-- <param>pragmatech.digital.workshops.lab8.experiment.LateReturnFeeCalculatorTest</param>-->
183+
<param>pragmatech.digital.workshops.lab8.experiment.LateReturnFeeCalculatorBadTest</param>
183184
</targetTests>
184185
<mutators>
185186
<mutator>DEFAULTS</mutator>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package pragmatech.digital.workshops.lab8.experiment;
2+
3+
import java.math.BigDecimal;
4+
import java.time.Clock;
5+
import java.time.Instant;
6+
import java.time.LocalDate;
7+
import java.time.ZoneOffset;
8+
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Tag;
11+
import org.junit.jupiter.api.Test;
12+
import pragmatech.digital.workshops.lab8.entity.Book;
13+
import pragmatech.digital.workshops.lab8.entity.BookStatus;
14+
import pragmatech.digital.workshops.lab8.service.LateReturnFeeCalculator;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
18+
/**
19+
* Intentionally bad test suite for {@link LateReturnFeeCalculator}.
20+
*
21+
* Line coverage: 100% — every branch is executed.
22+
* Mutation coverage: very low — assertions are too weak to detect
23+
* when PIT changes boundary conditions or rate constants.
24+
*
25+
* Surviving mutations include:
26+
* - Replacing {@code return BigDecimal.ZERO} with any non-null value (isNotNull passes)
27+
* - Changing {@code <= 7} to {@code < 7} (mid-tier day 4 stays in tier 1)
28+
* - Changing {@code <= 14} to {@code < 14} (mid-tier day 11 stays in tier 2)
29+
* - Swapping RATE_TIER_ONE for RATE_TIER_TWO (isPositive still passes)
30+
*/
31+
class LateReturnFeeCalculatorBadTest {
32+
33+
private static final Clock FIXED_CLOCK = Clock.fixed(
34+
Instant.parse("2025-06-01T00:00:00Z"),
35+
ZoneOffset.UTC
36+
);
37+
38+
private LateReturnFeeCalculator cut;
39+
private Book borrowedBook;
40+
41+
@BeforeEach
42+
void setUp() {
43+
cut = new LateReturnFeeCalculator(FIXED_CLOCK);
44+
borrowedBook = new Book("978-0-13-468599-1", "Clean Code", "Martin", LocalDate.of(2008, 8, 1));
45+
borrowedBook.setStatus(BookStatus.BORROWED);
46+
}
47+
48+
@Test
49+
void shouldReturnSomethingWhenBookIsNotBorrowed() {
50+
Book book = new Book("978-0-13-468599-1", "Clean Code", "Martin", LocalDate.of(2008, 8, 1));
51+
book.setStatus(BookStatus.AVAILABLE);
52+
53+
BigDecimal fee = cut.calculateFee(book, LocalDate.of(2025, 5, 1));
54+
55+
// covers the branch but isNotNull() passes even when PIT replaces
56+
// return BigDecimal.ZERO with return RATE_TIER_ONE.multiply(BigDecimal.valueOf(31))
57+
assertThat(fee).isNotNull();
58+
}
59+
60+
@Test
61+
void shouldReturnSomethingWhenBookIsReturnedOnTime() {
62+
BigDecimal fee = cut.calculateFee(borrowedBook, LocalDate.of(2025, 6, 1));
63+
64+
// covers daysOverdue <= 0 branch; isNotNull() survives any non-null return mutation
65+
assertThat(fee).isNotNull();
66+
}
67+
68+
@Test
69+
void shouldReturnPositiveFeeWhenBookIsOverdue() {
70+
// Day 4 — mid-tier 1; day 11 — mid-tier 2; day 20 — mid-tier 3.
71+
// All three overdue branches are executed → 100% line coverage.
72+
// But mid-tier inputs mean boundary mutations (<= 7 → < 7, <= 14 → < 14) go undetected,
73+
// and isPositive() cannot distinguish the wrong rate being applied.
74+
BigDecimal tierOneFee = cut.calculateFee(borrowedBook, LocalDate.of(2025, 5, 28)); // 4 days
75+
BigDecimal tierTwoFee = cut.calculateFee(borrowedBook, LocalDate.of(2025, 5, 21)); // 11 days
76+
BigDecimal tierThreeFee = cut.calculateFee(borrowedBook, LocalDate.of(2025, 5, 12)); // 20 days
77+
78+
assertThat(tierOneFee).isPositive();
79+
assertThat(tierTwoFee).isPositive();
80+
assertThat(tierThreeFee).isPositive();
81+
}
82+
}

slides/assets/qa-screen.jpg

946 KB
Loading

slides/lab-8.md

Lines changed: 47 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -498,112 +498,94 @@ Define your testing conventions in `.claude/CLAUDE.md` to guide AI code generati
498498

499499
# Workshop Summary
500500

501-
## Lab 1 — JUnit 5 & The Testing Pyramid
501+
## Lab 1 - Unit Testing with Spring Boot
502502

503-
- JUnit 5 annotations: `@Test`, `@BeforeEach`, `@Nested`, `@ParameterizedTest`, `@Tag`
504-
- Maven Surefire (unit) vs. Failsafe (integration) plugin split
505-
- Test categorization with `@Tag` and Maven profiles (`-Punit-tests`)
506-
- The testing pyramid: unit → integration → e2e; prefer fast tests at the bottom
503+
- The testing pyramid: write most tests as fast unit tests; reserve slow integration tests for critical paths only
504+
- JUnit 5/6 lifecycle annotations (`@BeforeEach`, `@Nested`, `@ParameterizedTest`, `@Tag`) and the extension model for cross-cutting concerns
505+
- Maven Surefire runs `*Test.java` (unit); Failsafe runs `*IT.java` (integration) - keeping feedback loops separate
506+
- `spring-boot-starter-test` bundles AssertJ, Mockito, and the Spring Test framework out of the box
507507

508508
---
509509

510-
# Workshop Summary
511-
512-
## Lab 2 — Sliced Testing & Testcontainers
510+
## Lab 2 - Sliced Testing: The Web Layer
513511

514-
- `@WebMvcTest` — loads only the web layer (controller + filters), fast and focused
515-
- `@DataJpaTest` — loads only JPA layer with in-memory or real DB
516-
- `@JsonTest` — tests JSON serialization/deserialization in isolation
517-
- `@RestClientTest` — tests HTTP clients with a mock server
518-
- Testcontainers with `@ServiceConnection` — real PostgreSQL in Docker, zero config
512+
- `@WebMvcTest` loads only controllers, filters, and security config - no service or repository beans - starts in under a second
513+
- `MockMvc` simulates HTTP requests inside the JVM without a real server: assert status codes, JSON paths, and headers
514+
- `@MockitoBean` stubs the service layer so controllers can be tested in complete isolation from the database
515+
- Spring Security is fully active: use `@WithMockUser`, `.with(jwt())`, and `.with(csrf())` per test
519516

520517
---
521518

522-
# Workshop Summary
523-
524-
## Lab 3 — WireMock & External APIs
519+
## Lab 3 - Sliced Testing: Persistence & HTTP Clients
525520

526-
- WireMock standalone for stubbing HTTP dependencies in tests
527-
- `ApplicationContextInitializer` pattern to wire WireMock into Spring context
528-
- Stub request/response matching: URL, headers, body matchers
529-
- `WireMock.verify()` to assert outbound HTTP calls were made
530-
- Separating stub definitions into dedicated stub classes for reuse
521+
- `@DataJpaTest` loads only the JPA layer and wraps every test in a transaction that rolls back automatically
522+
- Replace H2 with a real PostgreSQL container via Testcontainers + `@ServiceConnection` to test against the production database engine
523+
- `@JsonTest` verifies Jackson serialisation/deserialisation in isolation; `@RestClientTest` stubs HTTP responses via `MockRestServiceServer`
524+
- Each slice loads only what it needs - failures pinpoint the right layer and the feedback loop stays fast
531525

532526
---
533527

534-
# Workshop Summary
535-
536-
## Lab 4 — Best Practices & Mutation Testing
528+
## Lab 4 - Full Context Integration Testing
537529

538-
- Best practices: test readability, meaningful names, AAA structure
539-
- `@MockitoBean` vs `@MockBean` (Spring Boot 3.4+)
540-
- Spring Boot test slices deep dive: what each slice loads
541-
- PIT mutation testing: detecting weak assertions and missing branches
542-
- AI-assisted test generation: using LLMs as a TDD pair programmer
530+
- `@SpringBootTest` boots the entire `ApplicationContext` - every bean, security config, and Flyway migration - closest to production
531+
- Four challenges: outbound HTTP on startup, infrastructure dependencies, security, and data cleanup — each requiring a deliberate solution
532+
- WireMock via `ApplicationContextInitializer` stubs outbound HTTP before beans initialize; Testcontainers manages a real PostgreSQL container
533+
- `MOCK` (MockMvc, `@Transactional` rollback) vs. `RANDOM_PORT` (real TCP, real commits, manual `@AfterEach` cleanup)
543534

544535
---
545536

546-
# Workshop Summary
547-
548-
## Lab 5 — Advanced Spring Test Slices
537+
## Lab 5 - MockMvc, WebTestClient & Context Customisation
549538

550-
- `@SpringBootTest` full context vs. targeted slices
551-
- Custom test slices with `@AutoConfigureXxx`
552-
- `@TestConfiguration` for test-only beans
553-
- Security testing: `@WithMockUser`, `@WithSecurityContext`
554-
- Validation testing with `@SpringBootTest(webEnvironment = NONE)`
539+
- `MOCK`: test and server share the same thread → `@Transactional` wraps the whole call chain and rolls back automatically
540+
- `RANDOM_PORT`: real TCP to a separate Tomcat thread → `@Transactional` has no effect; use `@AfterEach deleteAll()` for cleanup
541+
- Customize the context per class with `@TestPropertySource`, `@ActiveProfiles`, `@TestConfiguration` + `@Import`, and `@Primary` beans
542+
- Prefer handwritten fakes with `@Primary` over `@MockitoBean` - every `@MockitoBean` variation forces a brand-new context startup
555543

556544
---
557545

558-
# Workshop Summary
559546

560-
## Lab 6 Spring Context Caching
547+
## Lab 6 - Spring Context Caching
561548

562-
- Spring caches `ApplicationContext` across tests sharing the same configuration
563-
- Context cache killers: `@DirtiesContext`, `@MockitoBean`, `@ActiveProfiles`, `@TestPropertySource`
564-
- The `SharedIntegrationTestBase` pattern: one base class → one cached context
565-
- Scout24 example: 12 contexts → 2 contexts, 45 min → 12 min build time
566-
- Use consistent `@Import`, `@ContextConfiguration` across all integration tests
549+
- Spring caches `ApplicationContext` using `MergedContextConfiguration` as the key - identical configs share one context across the suite
550+
- Before starting, Spring X-rays the test class and merges all annotations, initializers, `@MockitoBean` definitions, and property overrides into the key object; `hashCode`/`equals` decides hit or miss
551+
- Cache killers: `@DirtiesContext`, per-class `@MockitoBean`, varying `@ActiveProfiles`, inline `properties` on `@SpringBootTest`
552+
- The `SharedIntegrationTestBase` pattern consolidates all shared annotations into one abstract base class → one context for all integration tests
567553

568554
---
569555

570-
# Workshop Summary
571-
572-
## Lab 7 — Test Parallelization & CI Excellence
556+
## Lab 7 - Test Parallelisation & CI Excellence
573557

574-
- JUnit Jupiter parallel execution + Maven Surefire `forkCount` — complementary mechanisms
575-
- Safe integration test parallelization: `@Transactional` + UUID test data
576-
- Testcontainers: static `@Bean @ServiceConnection` singleton pattern
577-
- Connection pool exhaustion: reduce `hikari.maximum-pool-size` + maximize context reuse
578-
- GitHub Actions: `timeout-minutes`, `cache: maven`, `redirectTestOutputToFile`, `--fail-at-end`
558+
- JUnit Jupiter parallel execution (`junit-platform.properties`) + Maven Surefire `forkCount` are complementary and multiply each other's speedup
559+
- Safe parallel integration tests use UUID-prefixed test data or `@Transactional` rollback to prevent cross-test interference
560+
- Static `@Bean @ServiceConnection` Testcontainers fields share one container instance across all tests in the same context
561+
- GitHub Actions best practices: `timeout-minutes`, Maven cache, `--fail-at-end` to collect all failures, redirect test output to files
579562

580563
---
581564

582-
# Workshop Summary
583-
584-
## Lab 8 — General Testing Hacks
565+
## Lab 8 - General Testing Hacks & Libraries
585566

586-
- `OutputCaptureExtension` — capture stdout/logs in tests without Spring context
587-
- Mutation testing with PIT — find weak assertions and untested branches
588-
- Container log capture — `Slf4jLogConsumer` + `getLogs()` for debugging
589-
- `@RecordApplicationEvents` — assert Spring application events are published
590-
- `ApplicationContextRunner` — test conditional beans in milliseconds
591-
- ArchUnit — enforce layered architecture rules as executable tests
592-
- GreenMail — test email sending without a real SMTP server
593-
- TDD with AI — CLAUDE.md conventions, test-first prompting, Diffblue Cover
567+
- `OutputCaptureExtension`, `Slf4jLogConsumer`, `@RecordApplicationEvents`, `ApplicationContextRunner` - lightweight utilities for targeted testing without a full context
568+
- PIT mutation testing introduces code mutations automatically: 100% line coverage ≠ 100% confidence; strong assertions are essential
569+
- ArchUnit enforces layered architecture rules as executable unit tests in CI - soft conventions become hard failures
570+
- Object Mother pattern centralizes test data creation; GreenMail, Selenide, and Pact fill gaps that Spring Boot's built-in slices do not cover
594571

595572
---
596573

574+
![bg right:33%](assets/qa-screen.jpg)
575+
597576

598-
## Q & A
577+
## Time for General Q&A
599578

579+
- Do you have any questions about the exercises, the solutions, or the general testing tips and libraries we covered?
580+
- Are there any specific testing challenges you've faced in your projects that you'd like to discuss?
581+
- Would you like to see a live demo of any of the tools or techniques we covered?
600582

601583

602584
---
603585

604586
## Joyful Testing!
605587

606-
Workshop materials are on [GitHub](https://github.com/PragmaTech-GmbH/digdir-workshop/)
588+
Workshop materials are on [GitHub](https://github.com/PragmaTech-GmbH/digdir-workshop/).
607589

608590
The rendered slides are in the `slides/` folder.
609591

0 commit comments

Comments
 (0)