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

def test_registration_integration_real_sender(): # an INTEGRATION test - wires the real components together, no double at all real_sender = RealNotificationSenderV2() service = UserRegistrationService(real_sender) service.register("frank@example.com") # exercises the REAL seam directly
Verified directly — the same bug the mock missed, caught immediately by wiring the real seam together
Chapter 4's own mock-based test still passes: 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.
This doesn't make test doubles wrong — it makes them incomplete on their own
Chapter 4 didn't argue against mocks and fakes; it measured a real 443x speed advantage for them. The right read of both chapters together: doubles are for testing logic in isolation, fast — but at least one test per real seam needs to exercise the genuine dependency, or drift like Chapter 4's own bug has no way to ever surface before production.

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.

USER_SERVICE_CONTRACT = { 'id': int, 'email': str, 'active': bool, } def contract_test_user_service(provider): # runs against the REAL provider, needs no consumer payload = provider.get_user(1) return validate_against_contract(payload, USER_SERVICE_CONTRACT)
Verified directly — the contract test caught a provider-side rename with no consumer involved at all
Against 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.
Verified directly — what the contract test prevented
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.
A contract test isn't a substitute for the real integration test — it's a cheaper early warning
Passing the contract above only proves the shape matches; it doesn't prove 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 findingWhat it connects to
Integration test catching the exact bug the mock missedChapter 4's own mock-drift finding — this chapter is the direct fix
A shared schema checked independently on each side of a boundarySoftware 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

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

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