1. Consumer-Driven Contract Testing with Pact
Week 18's saga assumed inventory-service would keep publishing
InventoryReserved events in the shape order-service
expects. Nothing in either service's own test suite verifies that assumption — each
service's tests only prove it's internally correct, in isolation. A full end-to-end
environment with every service running would catch a mismatch, but it's slow, flaky,
and requires every team to coordinate a deploy just to run a test. Contract
testing verifies the same boundary without either extreme.
In consumer-driven contract testing, the consumer writes a test expressing exactly what it expects from the provider — this generates a machine-readable "pact" file — and the provider later replays that exact expectation against its real implementation to confirm it still holds.
@ExtendWith(PactConsumerTestExt.class)
class InventoryServiceContractTest {
@Pact(consumer = "order-service", provider = "inventory-service")
RequestResponsePact reserveInventoryPact(PactDslWithProvider builder) {
return builder
.given("SKU ABC123 has stock available")
.uponReceiving("a request to reserve inventory")
.path("/api/inventory/reserve")
.method("POST")
.body("""
{"sku": "ABC123", "quantity": 2}
""")
.willRespondWith()
.status(200)
.body(newJsonBody(o -> {
o.stringType("reservationId");
o.stringValue("status", "RESERVED");
}).build())
.toPact();
}
@Test
@PactTestFor(pactMethod = "reserveInventoryPact")
void reservesInventorySuccessfully(MockServer mockServer) {
var response = inventoryClient(mockServer.getUrl()).reserve("ABC123", 2);
assertThat(response.status()).isEqualTo("RESERVED");
}
}
Running this test produces a pact file — a JSON document literally describing "when
I send this request, I expect this response shape back." That file is published to a
shared Pact Broker, which inventory-service's own build
then downloads and replays:
@Provider("inventory-service")
@PactBroker(url = "https://pact-broker.acme.internal")
class InventoryServiceProviderTest {
@State("SKU ABC123 has stock available")
void setupStockAvailable() {
inventoryRepository.save(new Inventory("ABC123", 10));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
}
If inventory-service ever changes its response shape in a way that
breaks what order-service actually depends on, this provider-side
verification fails in inventory-service's own CI pipeline — before it's
ever deployed, and without order-service needing to be running anywhere
nearby.
The contract is written from the consumer's actual usage, not the provider's guess at what might be needed — this keeps the contract minimal and focused on real dependencies rather than testing every field the provider happens to expose. It's also the mechanism that lets a provider safely add new fields nobody depends on yet without breaking any contract, since consumer-driven contracts only assert on what's actually consumed.
2. Mutation Testing
Code coverage answers "did my tests execute this line?" It says nothing about whether
those tests would actually notice if that line's logic were wrong — a test that calls
a method and asserts nothing meaningful about its result achieves 100% coverage of
that method while catching zero bugs in it. Mutation testing answers
the sharper question directly: it automatically introduces small, deliberate bugs
("mutants") into your code — flipping a > to >=,
changing a + to -, negating a boolean — and reruns your test
suite against each mutated version.
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.16.1</version>
<configuration>
<targetClasses>
<param>com.codeverse.week22.*</param>
</targetClasses>
</configuration>
</plugin>
If a test suite catches the mutant — some test fails because the mutated behavior differs from what was asserted — the mutant is killed, a good outcome. If every test still passes despite the introduced bug, the mutant survives — a concrete, specific sign that no test actually verifies that piece of logic correctly, no matter what the coverage report claims.
// original
boolean isEligibleForDiscount(int orderTotal) {
return orderTotal > 100;
}
// PIT's mutant: flips > to >=
boolean isEligibleForDiscount(int orderTotal) {
return orderTotal >= 100;
}
// If this test suite's only test is isEligibleForDiscount(150) -> true,
// BOTH the original and the mutant pass it. The mutant SURVIVES --
// revealing that the boundary condition (orderTotal == 100) is
// completely untested, something 100% line coverage would never show.
That surviving mutant points to exactly the missing test: an assertion at
orderTotal = 100 itself, the boundary the two operators actually
disagree on. Mutation testing doesn't replace writing good tests — it's a tool for
finding the specific gaps a coverage report can't see.
PIT is genuinely slow — it reruns the full test suite once per mutant, which multiplies quickly across a large codebase. Scope targetClasses to business-critical logic (pricing, eligibility rules, the saga compensation logic from Week 18) rather than running it against getters, DTOs, and configuration classes where a surviving mutant tells you nothing useful.
3. Testing Resilience Under Fault Injection
Week 9's Resilience4j circuit breaker and retry configuration has never actually been tested against a real failure — it's been trusted to work because the configuration looks correct. WireMock, already familiar from mocking HTTP dependencies, can simulate the specific failure modes those resilience patterns exist to handle, proving the behavior rather than assuming it.
@Test
void fallsBackWhenInventoryServiceTimesOut() {
wireMock.stubFor(post(urlEqualTo("/api/inventory/reserve"))
.willReturn(aResponse()
.withFixedDelay(5000) // exceeds the configured timeout
.withStatus(200)));
ReservationResult result = inventoryClient.reserve("ABC123", 2);
assertThat(result.status()).isEqualTo(ReservationResult.Status.FALLBACK_UNAVAILABLE);
}
@Test
void circuitOpensAfterConsecutiveFailures() {
wireMock.stubFor(post(urlEqualTo("/api/inventory/reserve"))
.willReturn(aResponse().withStatus(500)));
for (int i = 0; i < 10; i++) {
inventoryClient.reserve("ABC123", 1); // trip the circuit breaker
}
// the circuit should now be OPEN -- calls fail fast without even
// hitting WireMock, which we can verify directly:
assertThat(circuitBreakerRegistry.circuitBreaker("inventoryService").getState())
.isEqualTo(CircuitBreaker.State.OPEN);
}
These tests aren't checking that your business logic is correct — they're checking that the resilience configuration itself behaves as designed: that a slow dependency actually triggers the fallback instead of hanging the caller, and that enough consecutive failures actually trip the circuit breaker rather than the threshold being silently misconfigured.
It's easy to write @CircuitBreaker/@Retry annotations, see the app start successfully, and assume the behavior is correct. Nothing about a clean startup proves the threshold, timeout, or fallback actually fire under the conditions they're designed for — a resilience pattern that's never been tested against a simulated failure is, in practice, no more trustworthy than one that isn't there at all.
4. Hands-on Exercise
Write a contract test, find a real mutation-testing gap, and prove resilience under fault
Apply all three practices to the services from Weeks 9–10 and 18.
Requirements:
- Write a Pact consumer test in one service expressing exactly what it expects from a real call to the other, and confirm the generated pact file matches the interaction.
- Write the corresponding provider verification test in the second service, and confirm it passes against the real implementation.
- Deliberately break the provider's response shape (rename a field) and confirm the provider verification test fails, catching the break before any deploy.
- Run PIT mutation testing against one class containing real business logic (not a DTO or getter), find at least one surviving mutant, and write the missing test that kills it.
- Write a WireMock-based test proving your Week 9 circuit breaker actually opens after the configured number of consecutive failures.
PIT's HTML report (generated under target/pit-reports) highlights every surviving mutant directly on the source line it mutated — read that report rather than the console summary, since the console only gives you an aggregate percentage with none of the specific, actionable detail.
5. Knowledge Check
Three quick questions. Expand each to check your answer.
Q1
Why does the consumer, not the provider, write the pact in consumer-driven contract testing?
Why does the consumer, not the provider, write the pact in consumer-driven contract testing?
Writing the contract from the consumer's real usage keeps it scoped to exactly what's actually depended on, rather than every field the provider happens to expose. It also means a provider can freely add new fields or capabilities that no consumer uses yet without breaking any contract — only changes to what's genuinely consumed cause a verification failure.
Q2
Why can a method have 100% line coverage and still have a surviving mutant?
Why can a method have 100% line coverage and still have a surviving mutant?
Line coverage only measures whether a line of code executed during a test run, not whether any assertion actually depended on that line's specific behavior being correct. A test can call a method and execute every line without asserting anything precise enough to notice if that line's logic were subtly wrong — which is exactly the gap a surviving mutant exposes.
Q3
Why doesn't a clean application startup prove a configured circuit breaker or retry policy actually works?
Why doesn't a clean application startup prove a configured circuit breaker or retry policy actually works?
Startup only confirms the annotations and configuration are syntactically valid and the application context loads — it never exercises the failure path the resilience pattern is designed to handle. Whether the timeout actually triggers a fallback, or the circuit actually opens after enough failures, can only be verified by deliberately simulating that failure, which is what the fault-injection tests in Section 3 do.