Integration Testing: Contracts & Boundaries
Software Testing Strategy
Chapter 5 · Integration Testing: Contracts & Boundaries
Chapter 4 ended on a real bug: a mock-based test stayed green while the actual dependency it stood in for had drifted incompatible, and production crashed. Integration testing is the direct answer — testing where two real components genuinely meet, instead of doubling the seam away. This chapter verifies that fix, then extends it to contract testing, a way to catch the same class of bug without needing the whole system running at once.
Testing the Real Seam
expectation_met = True. The identical scenario run as an integration test — UserRegistrationService wired directly to the real, updated RealNotificationSenderV2, no double anywhere — fails immediately: TypeError: RealNotificationSenderV2.send() missing 1 required positional argument: 'body'. Same bug, same code, one test catches it in CI before deploy and the other doesn't.
Contract Testing: Catching Drift Without the Whole System Running
A full integration test needs both real components running together. That's not always practical — especially across service boundaries, where a "provider" and its "consumers" might be developed, deployed, and owned by different teams entirely. Contract testing solves a narrower version of the same problem: define the shape both sides agree on, and check each side against that shape independently.
UserServiceV1 (returns {'id', 'email', 'active'}): contract test passes, zero errors. Weeks later, UserServiceV2 renames active to is_active during an unrelated provider-side refactor. The same contract test, run against UserServiceV2 alone: fails — ["missing field 'active'"]. The consumer, OrderService, was never started for this check.
OrderService.summarize_user() against UserServiceV1: "User 1 active: True", works correctly. Against UserServiceV2: crashed with KeyError: 'active'. The contract test catches the exact same class of break the consumer would eventually hit — but at the provider's own deploy time, independent of whether the consumer's own test suite happens to run soon enough to notice.
OrderService handles every value the contract allows correctly (an active: False user, for instance). A genuine end-to-end integration test still has a job the contract can't do — the two are complementary, not interchangeable, matching the exact tradeoff already established between the pyramid's own levels in Chapter 2.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Integration test catching the exact bug the mock missed | Chapter 4's own mock-drift finding — this chapter is the direct fix |
| A shared schema checked independently on each side of a boundary | Software Architecture Fundamentals Chapter 5's own service-boundary material, and Chapter 6's event-driven architecture, where a message schema plays exactly this same contract role |
Hands-On Exercises
Extend this chapter's own USER_SERVICE_CONTRACT with a new required field, 'created_at': str. Verify the contract test now fails against both UserServiceV1 and UserServiceV2 (neither currently returns this field), then add the field to UserServiceV1 and verify the contract test passes again.
Write a corrected UserRegistrationService that calls the new 3-argument send() signature, and a corresponding integration test wiring it to RealNotificationSenderV2. Verify the integration test now passes, confirming the fix rather than just detecting the break.
Add a type-mismatch case to this chapter's own contract test: create a UserServiceV3 that returns 'active' as the string "true" instead of the boolean True. Verify validate_against_contract correctly reports a type error for this case, distinct from the missing-field error found for UserServiceV2.
Chapter 5 Quick Reference
- Verified: an integration test wiring the real seam together caught Chapter 4's own mock-drift bug immediately, where the mock-based test stayed green
- Verified: a contract test using only a shared schema caught a provider-side field rename with the consumer never even running
- Contract testing's real value: an early, cheap warning at the provider's own deploy time, independent of the consumer's own test schedule
- Contract testing's real limit: proves the shape matches, not that every value in that shape is handled correctly — still needs real integration/e2e coverage alongside it
- Where this lives architecturally: Software Architecture Fundamentals' own service-boundary and event-driven material — a contract is the same kind of shared agreement, tested rather than just documented
- Next chapter: End-to-End & System Testing — the top of the pyramid, and the practical problem of flakiness