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.
Describe what a comprehensive testing approach for a REST API actually covers end to end: functional correctness, schema validation, error handling, authentication, and how contract testing fits alongside all of that. What tools would you reach for, and how does an exploratory pass differ from your automated suite?
Sample Answer
Direct answer
A comprehensive API testing approach layers several distinct concerns on top of each other: functional correctness first, then schema validation, then error handling, then auth, and finally the contract between this service and whoever consumes it, each one catching a different class of bug the others wouldn't.
Structured elaboration
Functional checks. Does each endpoint do what it's supposed to for valid input: correct status codes, correct data in the response, correct side effects (a POST actually creates what it claims to). This is the foundation everything else builds on.
Schema validation. Beyond "does the happy path work," does the response's SHAPE match what's documented or agreed, every time, not just in the cases someone happened to manually check. This catches drift between implementation and documentation that functional testing alone, if it only asserts on a few specific fields, can miss.
Error handling. Deliberately sending invalid, malformed, or edge-case input and confirming the API fails predictably and informatively, the right status code, a clear error message, rather than a generic 500 or, worse, silently accepting bad input and producing corrupted state.
Authentication and token flows. Confirming protected endpoints actually enforce auth (reject missing/invalid/expired credentials) and that the specific token lifecycle, issuance, refresh, expiry, revocation, behaves correctly, not just that a valid token happens to work once.
Rate-limiting behavior. Confirming the API actually throttles once a client exceeds its limit, and that it communicates that throttling clearly (a 429 with retry guidance) rather than degrading in some undocumented way.
Contract testing. Where this service has consumers (other services, or the same service's client SDK), consumer-driven contract tests confirm the service continues to honor what those consumers actually depend on, catching a class of regression, "this change is fine in isolation but breaks a real caller", that testing the service by itself can't see.
Tools. For manual and exploratory work, a tool like Postman is fast to work in interactively. For the durable, CI-enforced regression suite, a code-based framework (REST-Assured for a Java stack, pytest with requests for Python) integrates naturally with the rest of the codebase's testing and CI setup. For contract testing specifically, a dedicated tool like Pact handles the consumer/provider verification workflow that a general-purpose HTTP testing library doesn't natively support.
Exploratory vs. automated. Exploratory testing, poking at an API by hand, trying inputs nobody thought to write a formal test for, is how you DISCOVER what needs testing, especially early on or when integrating with something unfamiliar. The automated suite is where you PRESERVE that discovery so it keeps being checked on every future change; a bug found exploratorily that never gets turned into an automated test only protects you once.
Trade-offs and pitfalls
Treating this as one flat checklist to complete once, rather than as several genuinely different KINDS of testing each with their own cadence and depth, undersells how differently these axes need to be maintained: schema validation and functional tests belong on every CI run, contract tests need active coordination with real consumers to stay meaningful, and exploratory testing is an ongoing practice, not a phase you finish. Comprehensive doesn't mean "one big suite that does everything the same way," it means each of these concerns getting the specific kind of attention it actually needs.
Design a consumer-driven contract testing rollout for an organization with dozens to hundreds of microservices owned by different teams. Cover how contracts are authored and versioned, how they are stored and published, what a provider verification pipeline looks like, and how you would handle a backward-incompatible change without breaking a deployment.
Sample Answer
Direct answer
At organizational scale, a contract testing rollout has three parts working together: a clear authoring and versioning discipline for contracts, a broker as the shared source of truth, and CI gates on both the consumer and provider side that actually block a bad deploy rather than just reporting on it after the fact.
Structured elaboration
Authoring and ownership. Each consumer team owns the contracts that describe what it needs from a provider; each provider team owns making its own CI verify against every contract published against it. This is what keeps contracts from silently drifting out of sync: since a consumer's contract is generated by running its own test, it's grounded in what the code actually depends on, not documentation someone forgot to update.
Storage and versioning. A broker stores every contract, tagged by consumer version and branch, and every verification result, tagged by which provider version verified against which consumer version. This is the piece that scales the approach past a handful of services: instead of everyone needing to know everyone else's state, the broker answers "is it safe for provider version X to deploy, given what consumer versions are actually running in production right now" as a single query, sometimes called a can-I-deploy check.
CI integration. Two hooks matter. On the consumer side, a merge to main publishes the new contract to the broker. On the provider side, the pipeline pulls the latest relevant contracts and runs provider verification, replaying each contract's interactions against the real service, before allowing the build to proceed. Verification timing is usually split: on every pull request against the most recent contracts (fast feedback), and again on a schedule or before release against whatever's currently deployed (catching drift).
Handling backward-incompatible change. When a provider needs to make a breaking change, the discipline is to introduce the new behavior alongside the old one (an additive change, a new field, a new version), get every affected consumer to update and re-verify against the new shape, and only then retire the old behavior once the broker shows no live consumer still depends on it. The can-I-deploy check is what makes this safe to do incrementally rather than as a coordinated big-bang release.
Handling mismatches in CI. When provider verification fails, that failure belongs in the provider's own build, not the consumer's, and the pipeline should block the provider's deploy rather than let it ship and only fail visibly in production. Multiple consumers with conflicting expectations for the same interaction is a real failure mode: it needs to be resolved as a genuine compatibility conversation between teams, not silently overridden by whichever contract happened to verify last.
Trade-offs and pitfalls
The two failure modes worth naming explicitly: rolling this out with no governance, so every team invents its own conventions and the broker becomes noise instead of a source of truth, and rolling it out as pure tooling with no CI enforcement, so contracts exist but nothing actually blocks a bad deploy, which teaches everyone to ignore them. Retrofitting this onto an organization with many existing services and no prior contract-testing practice works better as a staged migration: pick a handful of well-understood, high-change-frequency service pairs first, prove the workflow and the can-I-deploy gate actually catch something real, then expand, rather than mandating it everywhere at once with no working example to point to.
Write a parameterized test that checks an API's input validation by sending it several different invalid payloads and confirming each one produces the expected status code and error message. How would you structure the test data so the suite stays maintainable as more invalid cases are added?
Sample Answer
Direct answer
Below is a parameterized pytest test that sends several distinct invalid payloads to an API and confirms each one produces the expected status code and error message, with the test data pulled out into a structured, named table rather than hardcoded per test case.
Structured elaboration
Externalizing the test data (a list of named cases, each with its own invalid payload and expected outcome) keeps the test readable as data grows: adding a new invalid-input case is a one-line addition to the table, not a new copy-pasted test function.
Worked example
import pytest
import requests
BASE_URL = "http://localhost:5000"
INVALID_PAYLOAD_CASES = [
pytest.param(
{"email": "not-an-email", "password": "secret123", "age": 30},
400, "invalid email format",
id="malformed_email",
),
pytest.param(
{"email": "ada@example.com", "password": "", "age": 30},
400, "password is required",
id="empty_password",
),
pytest.param(
{"email": "ada@example.com", "password": "secret123", "age": -5},
400, "age must be a positive integer",
id="negative_age",
),
pytest.param(
{"email": "ada@example.com", "password": "secret123"},
400, "age is required",
id="missing_age",
),
pytest.param(
{"email": "ada@example.com", "password": "secret123", "age": "thirty"},
400, "age must be a positive integer",
id="age_wrong_type",
),
]
@pytest.mark.parametrize("payload,expected_status,expected_error_substring", INVALID_PAYLOAD_CASES)
def test_input_validation_rejects_invalid_payloads(payload, expected_status, expected_error_substring):
resp = requests.post(f"{BASE_URL}/users", json=payload)
assert resp.status_code == expected_status, (
f"case with payload {payload}: expected {expected_status}, got {resp.status_code}"
)
body = resp.json()
assert expected_error_substring in body.get("error", "").lower(), (
f"case with payload {payload}: expected error containing "
f"'{expected_error_substring}', got '{body.get('error')}'"
)
Executed against a local Flask fixture implementing matching validation logic:
test_input_validation_rejects_invalid_payloads[malformed_email] PASSED
test_input_validation_rejects_invalid_payloads[empty_password] PASSED
test_input_validation_rejects_invalid_payloads[negative_age] PASSED
test_input_validation_rejects_invalid_payloads[missing_age] PASSED
test_input_validation_rejects_invalid_payloads[age_wrong_type] PASSED
5 passed in 0.09s
Why pytest.param(..., id=...) matters here. Naming each case explicitly (malformed_email, negative_age, and so on) is what makes a CI failure report actionable: test_input_validation_rejects_invalid_payloads[negative_age] FAILED tells you immediately which specific invalid input broke, versus an unnamed parametrize that would report only a numeric index.
Trade-offs and pitfalls
Asserting on an error message SUBSTRING rather than the full exact string is a deliberate choice: it lets the server's exact wording evolve (a copy change, a rephrased message) without breaking every test, while still confirming the error is actually ABOUT the right thing, catching a case where the server returns a generic "validation failed" for every input regardless of which field was actually wrong, which a substring check on the specific expected phrase would catch and a bare resp.status_code == 400 check would miss entirely.
How would you design tests for a real-time API built on WebSockets or gRPC streaming? Think about what a test harness for this looks like, how you'd get deterministic message ordering, how you'd verify reconnect and resume behavior, and how you'd test many concurrent streams at once.
Sample Answer
Direct answer
A real-time API test harness needs to think in terms of a persistent connection and a stream of messages over time, rather than the single request/response pair a normal API test assumes, which changes what "deterministic" and "correct" even mean for the test.
Structured elaboration
Harness architecture. Instead of a single request-then-assert, the harness opens a connection (a WebSocket, or a gRPC streaming call), sends and/or receives a SEQUENCE of messages over that connection, and needs to buffer and inspect that sequence rather than a single response. A typical shape: connect, send a subscribe or handshake message, collect incoming messages for a bounded window or until a specific terminal message arrives, then assert on the collected sequence.
Deterministic message sequences and IDs. Real-time systems often don't guarantee message arrival timing, so a test needs its assertions to be based on message CONTENT and IDs, not wall-clock timing. Giving each message a client-assigned correlation ID (or asserting on a server-assigned one) lets the test match "the response to message 3" reliably even if messages arrive close together or slightly out of the order they were logically generated.
Ordering guarantees. If the protocol promises ordering (messages for a given channel arrive in the order they were sent), the test should explicitly verify that promise rather than assume it, sending a sequence of numbered messages and asserting they're received in the same order is a direct way to test this.
Reconnect and resume. A realistic test deliberately drops the connection mid-stream and reconnects, then asserts on what happens next: does the client receive messages it missed while disconnected (if the protocol supports resume-from-a-point), or does it need to re-subscribe from scratch? This needs to be tested explicitly since it's exactly the scenario a naive implementation is most likely to get wrong, and it's the scenario most likely to occur in production intermittently, exactly when it's hardest to debug after the fact.
Backpressure. For a stream where the client can't keep up with the volume of incoming messages, the test verifies the system's actual backpressure behavior, whether it buffers, drops, or slows the sender, matches what's documented, rather than silently overwhelming the client and hiding the failure mode until it happens in production under real load.
Testing N concurrent streams. Beyond a single connection's correctness, testing many simultaneous connections verifies the system's behavior under realistic concurrency: does message delivery to one connection ever leak into another, does the system maintain per-connection state correctly, and does overall latency and throughput hold up as connection count grows.
Worked example
A minimal correlation-ID trace makes the "assert on content and IDs, not timing" guidance concrete. A client subscribes, then the server streams three numbered updates, and the test asserts on the sequence it actually received:
client sends: {"id": 1, "type": "subscribe", "channel": "prices"}
server sends: {"id": 1, "type": "ack"}
server sends: {"channel": "prices", "seq": 1, "value": 101.2}
server sends: {"channel": "prices", "seq": 2, "value": 101.5}
server sends: {"channel": "prices", "seq": 3, "value": 101.4}
The test does not assert anything about WHEN these arrive, only that message id=1 got an ack (confirming the subscribe was accepted before anything else happened), and that the three seq values arrived as [1, 2, 3] in that order with no gap and no repeat. A reconnect-mid-stream variant of the same trace would drop the connection after seq=2, reconnect, and then assert on whatever the protocol promises happens next: either seq=3 still arrives (a resume-from-a-point protocol) or the client re-subscribes and a fresh sequence starts from whatever the server considers current (a re-subscribe-from-scratch protocol). The test's job is to confirm which one the system actually does, not to assume either.
Trade-offs and pitfalls
The single biggest trap in testing real-time systems is asserting on WALL-CLOCK timing ("the response should arrive within 100ms") as if it were a hard correctness property. That kind of assertion is inherently flaky in CI, where resource contention can introduce timing variance that has nothing to do with whether the system is actually correct. Assertions should target message CONTENT, ORDER, and DELIVERY GUARANTEES, treating raw latency as a separate, explicitly-labeled performance concern with its own tolerance bands, not folded into the same test as correctness.
What does a solid test-data strategy for API and service tests actually look like in practice? Cover how you'd decide between generating data on the fly versus using fixed fixtures, when synthetic data is good enough versus when you need something closer to real production data, and how you keep tests from interfering with each other's data.
Sample Answer
Direct answer
A solid test-data strategy for API and service tests rests on four decisions made deliberately rather than by default: generate-on-the-fly versus fixed fixtures, synthetic versus production-derived data, how tests stay isolated from each other's data, and what actually needs cleanup versus what can be safely shared.
Structured elaboration
Generated on the fly vs. fixed fixtures. A fixture generated fresh for each test (a new user created at the start of the test, torn down at the end) gives strong isolation, no test can be affected by another test's leftover state, at the cost of some setup time per test. A fixed, shared fixture (a small set of known reference records seeded once) is faster to use repeatedly but risks one test's assumptions about that shared data becoming invalid if another test modifies it. The practical split: generate fresh data for anything a test needs to MUTATE, and use shared fixtures only for read-only reference data nothing is expected to change.
Synthetic vs. closer-to-production data. Purely synthetic data (templated names, generated emails) is simplest and carries no privacy risk, but can miss bugs that only show up with realistic value distributions or realistic correlations between fields. Production-derived data (masked or anonymized) is more realistic but requires real discipline around what's actually safe to use in a test environment, and is usually reserved for a smaller number of higher-fidelity tests (load testing, a final pre-release check) rather than the bulk of the day-to-day suite, where synthetic data's speed and simplicity matter more than its lower realism.
Isolation between tests. However the data is generated, tests running concurrently need to not collide: namespacing (a unique prefix or identifier per test run), using ephemeral per-test-run resources (a fresh database schema, a dedicated test namespace), or simply ensuring every generated record's identifiers are unique enough that two tests' data can coexist without either noticing the other.
Cleanup. Data a test creates should be removed when the test finishes, via the test's own teardown when things go well, and via a backstop (a scheduled sweep for anything matching a test-data naming pattern and older than some threshold) for the cases where a test crashes before its own teardown runs. Shared, read-only reference data doesn't need this kind of cleanup at all, that's part of why the mutate-vs-read-only split above matters: it determines which data even needs a cleanup story.
Trade-offs and pitfalls
The mistake that causes the most downstream pain is treating "test data strategy" as one uniform decision applied to everything, generate everything fresh, or share everything, rather than recognizing that different data has different needs. A shared reference fixture that's occasionally, accidentally mutated by a test that was supposed to only read it is one of the more confusing categories of flaky test to debug, since the failure shows up in a DIFFERENT, unrelated test that happened to run afterward and inherited the corrupted shared state, not in the test that actually caused the problem.
Unlock Full Question Bank
Get access to all 47 API and Contract Testing interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.