A shared infrastructure module is consumed by a dozen teams. What is a contract test on that module's interface, and what breakage does it catch that the module's own internal tests do not?
answer
- consumers depend on the surface, not the insides
- inputs, outputs, defaults, optionality
- a rename is a breaking change
- pin versions or the alarm is useless
basics
~20 sA contract test pins the module's published interface — its input names, types, defaults, required-ness and its outputs — and fails when a change breaks a consumer even though the module still works internally. Internal tests only check behaviour, not compatibility.
solid answer
~50 sA shared module's interface is its inputs and its outputs, and consumers depend on that surface, not on the resources inside. A contract test asserts the surface: these inputs exist with these names and types, these have defaults so they stay optional, these outputs are published with this shape. The module's own tests happily pass while you rename an input, tighten a type, make an optional variable required, or stop exporting an output — every one of which breaks a consumer at their next run. In practice you get most of the value from two cheap things: an interface snapshot that fails review when the surface changes without a version bump, and a small set of example configurations, each representing a real consumer's usage shape, that must still resolve after every change. Combine that with semantic versioning of the module and consumers pinning versions, so a genuinely breaking change is a major release rather than a surprise on Monday morning.
go deeper
Understand that a shared module's inputs and outputs are a promise to other teams, and that renaming or removing one breaks their code even if the module itself still works.
Be able to list what belongs to the contract — names, types, optionality, defaults, outputs, validation ranges — and explain why an interface snapshot in the repository turns a silent break into a review decision.
Show the operational half: versioned releases, consumers pinning ranges, example configurations that mirror real usage, and the recognition that a restructure can destroy consumers' resources without touching the interface at all.
Own the platform-team question — how many shared modules to maintain, what compatibility guarantee you publish, how deprecations are communicated and enforced, and who absorbs the cost when a major version has to roll out across a dozen teams.
## The problem contract testing solves When a module is used by one team, its tests and its consumers are the same codebase; a break is found immediately. When it is used by a dozen teams, the module's repository and its consumers' repositories move independently. The module's maintainer can make a change that is entirely correct by the module's own tests and entirely destructive to everyone downstream. The key insight is that consumers do not depend on the module's internals at all. They depend on its **interface** — the inputs they pass and the outputs they read. Everything else is implementation detail the module is free to change. So the thing to test is the interface, deliberately and separately from behaviour. ## What counts as the contract - **Input names.** A rename is a break, full stop. - **Input types.** Widening is usually safe; narrowing is a break. Turning a loosely typed input into a structured object with required fields breaks every caller passing the old shape. - **Optionality.** Removing a default makes an input required, which breaks every consumer who was relying on the default. - **Default values.** Changing a default is subtler and often worse than a hard break: nothing errors, and consumers who never set the value silently get different infrastructure on their next run. - **Output names and shape.** Consumers wire outputs into other components; deleting or renaming one breaks their configuration, and changing its type breaks whatever consumes it downstream. - **Validation rules.** Tightening an accepted range rejects inputs that previously worked. - **Resource identity.** Not strictly interface, but adjacent and vicious: restructuring the module internally can cause consumers' existing resources to be destroyed and recreated on upgrade, even though every input and output is unchanged. ## How you actually test it Two mechanisms carry most of the weight, and neither is exotic. **An interface snapshot.** Generate a machine-readable description of the module's inputs and outputs — name, type, default, required, description — and commit it. Regenerate it in the pipeline and fail if it differs from the committed copy. That does not stop a breaking change; it forces the change to appear in the diff, where a reviewer must consciously accept it and bump the major version. It is cheap, it never flakes, and it converts silent breakage into a review conversation. **Example consumer configurations.** Keep a small directory of examples, each one shaped like a real consumer's usage: the minimal call with only required inputs, the fully specified call, and one or two awkward real-world shapes. Every change must still resolve and plan against all of them. This catches the cases a snapshot cannot express — an input that is technically still there but no longer accepts the value a consumer passes, or a combination that now conflicts. Those examples double as documentation, which is one of the few places in IaC where a test genuinely pays twice. ## Where versioning does the rest of the work Contract tests are only half the answer. The other half is release discipline: publish the module as versioned releases, have consumers pin to a version range rather than to a moving branch, and treat any contract-test failure as the signal to cut a major version. The contract test tells you a break happened; semantic versioning is what makes the break harmless, because consumers upgrade when they choose. Without pinning, a contract test is a smoke alarm in a house with no doors — you learn about the break at the same moment everyone else does. ## Interview framing The distinction to state plainly is *behaviour versus compatibility*. Internal module tests answer "does this module still build the right infrastructure?". Contract tests answer "can everyone who already calls this module still call it?". Those are different questions, they fail on different changes, and a widely shared module needs both. Add the point about defaults changing silently — it is the failure mode people who have actually maintained a shared module always bring up, and it is the one candidates who have not maintained one never mention.
- Why is changing a default value sometimes worse than removing an input entirely?Removing an input fails loudly at the consumer's next run, so it gets fixed. Changing a default fails silently: nobody's configuration errors, but every consumer who relied on the default now provisions something different — a bigger instance, a different retention period, encryption off. It surfaces later as unexplained infrastructure change or cost, with no obvious link to your release.
- How do you handle a rename you genuinely need to make?Do it as a deliberate major version with a migration note, not a silent edit. Where the module system allows it, ship the new input alongside the old for one release so consumers move on their own schedule, then remove the old one in the following major. And publish what consumers must do — a breaking change with instructions costs an afternoon; one without costs a week of support.
- Do contract tests need to deploy anything?No, and that is the point. Interface snapshots are generated from the module's declarations, and example configurations only need to resolve and plan. Both run in seconds without credentials or an account, which is why this discipline is affordable on every commit while a real deployment test is not.
saying these in an interview costs you the question
- Assuming internal module tests protect consumers
- Treating an input rename as a non-breaking change
- Letting consumers track a branch rather than a version
- Changing a default and calling it a minor release