API and Contract Testing Questions
Testing services and their interfaces directly. Covers REST and other API testing, request/response and schema validation, status and error handling, and contract testing between producers and consumers. Includes service-level and integration testing without a UI.
Walk through how a consumer-driven contract testing tool like Pact actually operates end to end: how the consumer test generates a contract, how that contract gets published and versioned, and how the provider verifies against it in its own CI. Where does this approach break down, and how would you introduce it to a team that has never used it before?
Sample Answer
Direct answer
Mechanically, a Pact-style workflow has four moving parts: the consumer writes a test against a local mock of the provider, that test's run produces a contract file, the contract gets published to a broker, and the provider's own CI pulls the latest contracts and verifies its real implementation against each one.
Structured elaboration
1. Consumer side. The consumer team writes a normal-looking test, but instead of hitting a real provider, it hits a mock server that the testing library spins up locally. The test tells the mock "when you get this request, respond with this." The consumer's actual client code makes the call, and the test asserts on what comes back. Because the mock only knows how to answer the interactions the test set up, running the test also produces a JSON file describing exactly those interactions: the contract (Pact calls it a "pact file").
2. Publishing. The consumer's CI pipeline pushes that contract to a broker, tagged with information like which consumer version and branch produced it.
3. Provider verification. The provider's CI pulls every contract published against it (from every consumer) and, for each one, replays the interaction: it sets up whatever state the contract's scenario needs, this is the "provider state," a hook the provider team writes, for example "given a user with ID 42 exists," then makes the request the contract describes against the real provider code and checks the response matches what the contract expects.
4. Versioning and tagging. Every verification result gets recorded against the specific consumer version and provider version involved. That's what lets the broker answer the operational question that actually matters: "if I deploy provider version X, is it currently compatible with whatever consumer versions are live in production?" Tags (like "main" or "production") let this check track what's actually deployed rather than every historical version.
False positives and negatives to watch for: a consumer that asserts on fields it never actually uses will fail verification when the provider changes something harmless, false positive from the contract's point of view even though nothing broke. A consumer that fails to assert on a field it silently depends on will let a real break through unverified. The discipline that keeps this workflow honest is asserting on exactly, and only, what the consumer's code reads.
Trade-offs and pitfalls
Two limitations come up often in practice. First, schema evolution: contracts pin a specific shape, so a provider that legitimately wants to evolve its schema (adding a field, tightening a type) needs an explicit compatibility policy, otherwise every provider change becomes a scramble to get every consumer to update its contract. Second, at real scale, a popular provider can accumulate a large contract surface: dozens of consumers, each with several scenarios, and provider verification time and coordination overhead both grow with that surface. Teams manage this with tagging and "can I deploy" checks (only re-verify against what's actually in production, not every historical contract) and with clear ownership so a provider team isn't blocked waiting on an unresponsive consumer team. Introducing this into an existing frontend/backend relationship usually goes smoother when it starts on one well-understood endpoint pair rather than a big-bang rollout, and when both sides agree up front on who owns writing and maintaining the tests. A contract that's too rigid, asserting more than the consumer needs, is a common early mistake, since it makes contract tests brittle in exactly the way they were meant to avoid, and it tends to happen when a contract is written to describe an entire response shape instead of just the fields actually consumed.
You depend on a downstream API that is slow and occasionally unreliable, and you want to isolate your tests from it without your stub silently drifting away from what the real service actually does. How would you set up and maintain that stub so it stays a faithful stand-in for the real API's contract?
Sample Answer
Direct answer
The goal isn't just to make the test pass against a fake version of the dependency, it's to make sure that fake stays faithful to what the real API actually does, so a passing test still means something. That takes a deliberate process for creating the stub and keeping it honest, not just hand-writing a plausible-looking response once.
Structured elaboration
Establishing a real baseline. Rather than guessing what the dependency's response looks like, record it from a real call (against a sandbox if one exists, or a controlled real call otherwise) and derive the stub's default behavior from that recording. This is what makes the stub a faithful stand-in rather than a convenient fiction.
Parameterizing the stub. A real dependency doesn't return the same thing for every input, it has different responses for different requests, and different failure modes (timeouts, specific error codes, malformed data). Configuring the stub to vary its response based on the incoming request, rather than always returning one canned reply, is what lets your tests exercise those different paths deliberately.
Stateful scenarios. Some dependencies have behavior that depends on sequence, a downstream service that returns "pending" the first few times you poll it and then "complete." A stub that can only return one fixed answer can't test that kind of flow; the stub needs to model a small state machine of its own to stand in convincingly.
Keeping the stub current. The real risk with any stub is drift: the real API changes and the stub quietly doesn't, so tests keep passing against a fiction. Mitigations that actually work: periodically re-recording the baseline against the real (or sandboxed) dependency and diffing it against the current stub configuration, and, where the provider publishes a machine-readable contract or schema, validating the stub's responses against that schema so a structural drift fails loudly instead of silently.
A concrete example. Configuring a stub for a slow downstream payment service, you'd want a mapping that returns a normal success response for the common case, an explicit configuration to simulate added latency (to test how your own service behaves under a slow dependency), and a specific trigger, keyed off something in the request itself, like a distinctive test value in place of a real identifier, that returns an error response, so a test can deliberately exercise the failure path without depending on the real dependency actually failing on demand.
Trade-offs and pitfalls
A stub that's too permissive (accepts anything, always returns success) gives false confidence: your tests pass, but they've stopped testing anything real about the integration. A stub that's too rigid (hardcoded to one exact request shape) breaks the moment your own client code changes something incidental, punishing you for changes that have nothing to do with the dependency. The balance point is a stub that's specific enough to catch a real contract violation but flexible enough not to be brittle against your own code's harmless changes, and periodically checked against the real dependency so that balance doesn't quietly decay.
Write a script that compares two API contract manifests (each describing endpoints, HTTP methods, required parameters, and response fields) and reports the differences between them. What normalization would you need to do first, and how does the approach scale as the manifests grow large?
Sample Answer
Direct answer
Below is a script that loads two API contract manifests and reports the differences between them: endpoints only in one, methods that changed, required parameters added or removed, and response fields added or removed.
Structured elaboration
The core idea is normalizing both manifests into the same comparable structure (a dictionary keyed by (path, method)) before diffing, so the comparison logic doesn't care what order the manifest's entries happened to be listed in.
Worked example
def normalize_manifest(manifest: list[dict]) -> dict:
"""Key each endpoint entry by (path, method) for order-independent comparison."""
return {(entry["path"], entry["method"]): entry for entry in manifest}
def diff_manifests(old_manifest: list[dict], new_manifest: list[dict]) -> dict:
old = normalize_manifest(old_manifest)
new = normalize_manifest(new_manifest)
old_keys = set(old.keys())
new_keys = set(new.keys())
report = {
"added_endpoints": sorted(new_keys - old_keys),
"removed_endpoints": sorted(old_keys - new_keys),
"changed_endpoints": {},
}
for key in sorted(old_keys & new_keys):
old_entry, new_entry = old[key], new[key]
old_params, new_params = set(old_entry.get("params", [])), set(new_entry.get("params", []))
old_resp, new_resp = set(old_entry.get("response", [])), set(new_entry.get("response", []))
changes = {}
if old_params != new_params:
changes["params_added"] = sorted(new_params - old_params)
changes["params_removed"] = sorted(old_params - new_params)
if old_resp != new_resp:
changes["response_fields_added"] = sorted(new_resp - old_resp)
changes["response_fields_removed"] = sorted(old_resp - new_resp)
if changes:
report["changed_endpoints"][f"{key[1]} {key[0]}"] = changes
return report
if __name__ == "__main__":
old_manifest = [
{"path": "/users", "method": "GET", "params": ["q"], "response": ["id", "name"]},
{"path": "/orders", "method": "POST", "params": ["userId"], "response": ["orderId"]},
]
new_manifest = [
{"path": "/users", "method": "GET", "params": ["q", "limit"], "response": ["id", "name", "email"]},
{"path": "/products", "method": "GET", "params": [], "response": ["id", "price"]},
]
import json
print(json.dumps(diff_manifests(old_manifest, new_manifest), indent=2))
Executed output:
{
"added_endpoints": [["/products", "GET"]],
"removed_endpoints": [["/orders", "POST"]],
"changed_endpoints": {
"GET /users": {
"params_added": ["limit"],
"params_removed": [],
"response_fields_added": ["email"],
"response_fields_removed": []
}
}
}
Normalization. Keying by (path, method) rather than comparing the manifests as raw lists is what makes the comparison correct regardless of ordering, without it, a manifest with the same endpoints listed in a different order would show up as entirely different.
Trade-offs and pitfalls
Complexity at scale. As written, this is O(n) in the number of endpoints, each manifest is normalized into a dict once (O(n)), and the diff itself is set operations over the keys, so it scales linearly and stays fast even for a manifest with thousands of endpoints. The part that DOESN'T scale as cleanly is a manifest where params or response fields are deeply nested objects rather than flat lists of names, this script's set-based comparison only detects a field being added or removed at the top level; a genuinely nested schema diff (a field's own sub-structure changing) needs a recursive comparison, which is real added complexity worth scoping in deliberately rather than assuming this flat version already handles it.
A service you depend on keeps shipping changes that break your integration with it, and the breakage is only caught after it reaches production. Propose both a technical and a process fix: what would you put in place so a breaking change is caught before it ships, who should own the tests that catch it, and how would you pilot the change and show it actually reduced regressions?
Sample Answer
Direct answer
The fix has to work on two tracks at once: a technical track that catches a breaking change before it ships, and a process track that makes catching it everyone's default rather than something only found in a postmortem. Neither alone is durable, technical checks without ownership get bypassed under deadline pressure, and process without automation depends on someone remembering to look.
Structured elaboration
Technical track. Add consumer-driven contract tests to the dependency relationship: the depending service's real expectations get encoded as a contract, and the upstream service's CI verifies against it before every deploy. This is what actually blocks a breaking change from shipping, rather than catching it after the fact in a bug report. Where a contract test isn't practical to set up quickly, an automated compatibility check that diffs the API's schema or response shape between versions is a cheaper first step that still catches the most common failure mode: a field silently removed, renamed, or retyped.
CI policy. The upstream service's pipeline should fail the build, not just warn, when provider verification against a downstream consumer's contract fails. Placement matters: this check belongs in the upstream service's own CI, where the change originates, not bolted onto the downstream service's pipeline as an afterthought.
Process track. Even a working technical check needs an owner. A PR template or review checklist item that asks "does this change anything a documented consumer depends on" catches the cases contract tests haven't been written for yet. Clear ownership, who's accountable when a contract test fails, and who's accountable for keeping the downstream service's contract accurate as its own needs evolve, is what prevents the check from silently rotting once nobody's paying attention to it.
Retrofitting onto an org with no prior governance. When there's no existing discipline at all, start with a migration plan rather than a mandate: pick the highest-incident-risk dependency pairs first (the ones that have actually caused regressions), prove contract testing catches something real there, and use that as the case for expanding rather than rolling it out everywhere simultaneously with no working example.
Piloting and measuring. Run the new discipline on one or two service pairs first. Track something concrete before and after: the number of contract-related production incidents on those pairs, or the number of contract-test failures caught in CI versus caught in production. That comparison is what actually demonstrates the pilot worked, rather than assuming it did because the process feels more rigorous.
Trade-offs and pitfalls
The most common way this fails is treating it as a one-time cleanup instead of an ongoing discipline: teams write contract tests once, the regressions stop for a while, and the checklist item quietly gets skipped under the next deadline because nothing enforces it anymore. Ownership needs to be durable, not a one-time assignment, and the CI gate needs to actually block a deploy, not just post a warning that's easy to ignore.
Your product integrates with a third-party API that has strict rate limits and charges per call, so you can't freely hit it from your test suite. Describe a testing strategy that gives you real confidence in the integration without incurring high cost or violating the provider's limits.
Sample Answer
Direct answer
The core idea is to spend real calls to the actual third-party API sparingly and deliberately, while getting most of your test coverage from something you control: a recorded baseline, a local emulation, or a contract test, and reserve the live provider for a small number of smoke tests that specifically verify the real integration still works.
Structured elaboration
Layered strategy.
- Contract or schema tests against a stored, versioned description of what the provider promises (often the provider's own published API spec, if they have one) catch most structural drift, a field renamed, a type changed, cheaply and without hitting the network at all.
- Record-and-replay captures real responses once (usually from a sandbox or a low-volume manual run) and replays them for the bulk of your test suite. This gets you realistic response shapes without repeated real calls, though it needs a process for refreshing the recordings when the provider's real behavior changes.
- Emulation or a sandbox environment, most billing and payment providers offer one, lets you exercise realistic flows (including error paths that are hard to trigger against the recorded baseline, like a declined charge) without touching real money or counting against production rate limits.
- A small number of selective real calls, run on a schedule rather than on every commit, act as your canary: if the real provider's behavior has drifted from what your recordings and contract assume, this is what catches it, ideally before a customer does.
A concrete instance of this, using a payments provider like Stripe as an example: most CI runs use Stripe's own test-mode sandbox, which behaves like the real API without moving real money, and this is usually sufficient for asserting your integration logic end-to-end. For scenarios where even the sandbox is inconvenient (say, you want a test to run fully offline, or you want to simulate a specific error the sandbox doesn't easily trigger on demand) local mocks, built from previously recorded sandbox responses, fill the gap. The trickiest part is often webhooks: a provider like Stripe delivers events asynchronously to a URL you register, and reliably testing that path means simulating the provider's webhook delivery yourself (many providers, including Stripe, publish a CLI or library specifically for this) rather than assuming your handler works just because the direct API calls do.
Trade-offs and pitfalls
The fidelity-versus-speed trade-off runs through every layer here: real calls are the most faithful and the slowest and riskiest to run often; a stale recording is fast but can silently drift from reality if nothing refreshes it. The discipline that keeps this from rotting is treating the recorded baseline as something with an owner and a refresh cadence, not a one-time snapshot, and keeping the scheduled real-call smoke tests as the backstop that would actually notice if the recordings had gone stale.
Unlock Full Question Bank
Get access to all 9 API and Contract Testing interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.