A component library release passed all its own tests, yet the recruiter pipeline board in a consuming app broke on upgrade; what are consumer-contract tests, and how would they have caught it?
answer
- authors test what they imagine
- consumers use what they need
- encode real usage as tests
- run against the release candidate
- public behavior only, never internals
basics
~10 sConsumer-contract tests encode what real consuming screens rely on and run against a library release candidate before publishing, catching breaks in usage the library's own authors never tested.
solid answer
~40 sLibrary tests encode what the authors think a component promises; consumers depend on combinations, sequences and extension points the authors never imagined. The pipeline board probably relied on something like the candidate card's selection callback firing once per click with the candidate's id. A **consumer-contract test** captures that reliance as a test. Two forms work in practice: consumers contribute small tests of public behavior into the library's own suite, where they run on every change; and the library runs selected consumers' component-facing tests against each release candidate before publishing. When one fails, triage it: a library regression gets fixed or versioned as breaking, while a test pinning undocumented internals gets rewritten - and may reveal a hook the library should offer.
go deeper
Recall that a library's own tests reflect its authors' assumptions, and that consumer-contract tests capture what real apps rely on.
Explain the forms a contract can take - contributed tests, release-candidate runs, a public-surface report - and what each catches.
Walk through selecting consumers, gating releases on candidate runs, and triaging a failure into a library regression or a consumer relying on internals.
Weigh the coordination cost of running other teams' tests against the cost of breaks reaching production, and decide who owns contributed contracts over time.
## Why library-owned tests miss consumer breaks A **component library**'s own tests are written by the people who built the components. They check the behavior the authors intended, in the combinations the authors thought of. Consuming apps use components in ways the authors did not predict: a candidate card nested inside a draggable column, a selection callback that drives a side panel, an action menu opened from a keyboard shortcut. When a release changes something in that unexplored space, every library test stays green and the recruiter pipeline board breaks in production. The gap is structural. It is not solved by writing more tests of the same kind, because the missing tests are about usage only consumers know. ## What a consumer-contract test is A **consumer-contract test** is a test that encodes what a specific consumer relies on in the library's public API, and runs against new library versions before they reach that consumer. The idea comes from contract testing between services, where the consumer states its expectations and the provider verifies them. For a UI library, a contract might say: 'when a candidate card in selectable mode is clicked, its selection callback fires exactly once, with the candidate's id, after the card reports itself selected.' ## Three forms | Form | How it works | Catches | Cost | |---|---|---|---| | **Contributed contracts** | Consumers add small tests of public behavior to the library's suite | Specific reliances, on every change | Low; needs review for scope | | **Release-candidate runs** | The library builds a candidate and runs selected consumers' component-facing tests against it | Breaks in real screens, including unforeseen combinations | Higher; depends on other teams' suites | | **Public-surface report** | A generated record of each component's properties, defaults and events, diffed on every change | Unnoticed API changes | Low; catches signature-level changes only | The three complement one another: the report catches changes to the declared surface, contributed contracts catch known reliances quickly, and candidate runs catch the unknown ones. ## Running it in practice 1. **Choose consumers** by usage and criticality - the screens that use the most components, and the flows where failure costs most. For a recruiting tool, the pipeline board and the interview scheduler rather than a rarely used settings page. 2. **Scope their tests** to those that exercise library components, so candidate runs stay fast enough to be routine. 3. **Run on release candidates**, not on every commit, and make a failing run block publishing until triaged. 4. **Triage every failure** into one of two kinds: - a **library regression** - fix it, or if the change is intended, ship it as a breaking change with a clear change entry; - a **consumer relying on internals** - the test is rewritten to assert public behavior, and the library considers offering the documented hook the consumer was reaching for. 5. **Promote recurring reliances** into contributed contracts, so they run on every change instead of only at release time. ## Pitfalls - **Letting contracts pin internals.** A contract on undocumented markup freezes the library's implementation; review contributed tests for public-behavior scope. - **Flaky consumer suites blocking releases.** Quarantine unstable consumer tests quickly, or the library team learns to ignore the layer. - **Treating contracts as a replacement** for the library's own interaction tests. Contracts cover what consumers exercise, which is never the whole promise. - **No owner for a contributed test.** When the consuming team moves on, a contract nobody understands becomes noise; record who owns each one. ## Beyond the web The model is platform-neutral. A design system shipping native mobile components can accept contributed contracts from app teams in the same way, and run the apps' UI tests against a candidate build of the component package. What changes is the build plumbing, not the idea: consumers state what they rely on, and the library proves it still holds before anyone upgrades.
- A contributed contract test fails because it relied on internal markup. Who fixes it?Usually the consumer, because the test pinned something outside the declared public API. But the failure is information: if they needed that detail, the library probably lacks a documented hook. Accept the contract back only once it asserts public behavior, and consider adding the hook they were reaching for.
- Running consumers' suites before each release is slow. How do you scale it?Pick a small set of consumers by usage and criticality, run only their tests that touch library components, and run them on release candidates rather than every change. Contributed contracts are small and live in the library's suite, so those can run on every change.
- How do contract tests differ from the library's own interaction tests?Interaction tests encode what the library's authors think a component promises. Contract tests encode what consumers actually rely on - the combinations, sequences and extension points real screens use. The overlap is large, but the gap between them is exactly where upgrade breaks hide.
saying these in an interview costs you the question
- If the library's own tests pass, no consuming app can break.
- Consumer contract tests may assert on internal markup if it is stable.
- Contract tests make the library's own interaction tests unnecessary.
- A failing consumer test always means the library has a bug.
- Every consumer's full suite must pass on every library commit.