1. Consumer-Driven Contract Testing with Pact
Week 18's saga assumed inventory-service would keep responding to
reservation requests 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.
import { PactV3, MatchersV3 } from "@pact-foundation/pact";
const provider = new PactV3({ consumer: "order-service", provider: "inventory-service" });
test("reserving inventory", async () => {
provider
.given("SKU ABC123 has stock available")
.uponReceiving("a request to reserve inventory")
.withRequest({
method: "POST",
path: "/inventory/reserve",
body: { sku: "ABC123", quantity: 2 },
})
.willRespondWith({
status: 200,
body: {
reservationId: MatchersV3.string("res_abc123"),
success: true,
},
});
await provider.executeTest(async (mockServer) => {
const response = await reserveInventory(mockServer.url, "ABC123", 2);
expect(response.success).toBe(true);
});
});
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:
import { Verifier } from "@pact-foundation/pact";
test("validates the expectations of order-service", () => {
return new Verifier({
provider: "inventory-service",
providerBaseUrl: "http://localhost:8001",
pactBrokerUrl: "https://pact-broker.acme.internal",
publishVerificationResult: true,
providerVersion: "1.4.2",
stateHandlers: {
"SKU ABC123 has stock available": async () => seedStock("ABC123", 10),
},
}).verifyProvider();
});
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 with Stryker
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 function and asserts nothing meaningful about its result achieves 100%
coverage of that function 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.
{
"mutate": ["src/billing/**/*.ts"],
"testRunner": "vitest",
"reporters": ["html", "clear-text"]
}
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
function isEligibleForDiscount(orderTotal: number): boolean {
return orderTotal > 100;
}
// Stryker's mutant: flips > to >=
function isEligibleForDiscount(orderTotal: number): boolean {
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.
Stryker is genuinely slow — it reruns the full test suite once per mutant, which multiplies quickly across a large codebase. Scope mutate to business-critical logic (pricing, eligibility rules, the saga compensation logic from Week 18) rather than running it against route handlers, DTOs, and configuration where a surviving mutant tells you nothing useful.
3. Testing Resilience Under Fault Injection
Week 9's retry and timeout configuration has never actually been tested against a
real failure — it's been trusted to work because the configuration looks correct.
nock (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.
import nock from "nock";
test("falls back when inventory-service times out", async () => {
nock("http://inventory-service")
.post("/reserve")
.delay(5000) // exceeds the configured timeout
.reply(200);
const result = await inventoryClient.reserve("ABC123", 2);
expect(result.status).toBe("FALLBACK_UNAVAILABLE");
});
test("circuit opens after consecutive failures", async () => {
nock("http://inventory-service")
.post("/reserve")
.times(10)
.reply(500);
for (let i = 0; i < 10; i++) {
await inventoryClient.reserve("ABC123", 1); // trip the circuit breaker
}
// the circuit should now be OPEN -- calls fail fast without even
// hitting nock, which we can verify directly:
expect(circuitBreaker.opened).toBe(true);
});
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 wrap a call in a retry/circuit-breaker library, 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 17–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 Stryker against one module containing real business logic (not a route handler or DTO), find at least one surviving mutant, and write the missing test that kills it.
- Write a
nock-based test proving a retry or circuit breaker you've configured actually behaves as designed under a simulated failure.
Stryker's HTML report 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 mutation score 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 function have 100% line coverage and still have a surviving mutant?
Why can a function 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 function 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 configuration is syntactically valid and the application 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.