The Sandbox Developer

When HTTP Mocks Are the Right Choice

Stateless mocks work for unit tests but fail at workflows that need memory.

Senior Contributor · · 10 min read
Cover illustration for “When HTTP Mocks Are the Right Choice”
Mocking vs. Stubbing · October 12, 2026 · 10 min read · 2,225 words

An HTTP mock is a server that returns predefined responses to matched requests. It carries no business logic, no database, and no memory of prior calls. A mock is a contract simulation: it exists to confirm that a request matches an expected shape and that a response conforms to an agreed schema, nothing more. It does not model a system. It models an agreement about what a system's interface looks like at a single point in time.

A well-configured mock has a specific and useful set of capabilities. The same endpoint can be toggled between a 200 OK, a 500 Internal Server Error, and an empty-state payload in a matter of seconds, something a live API cannot safely offer on demand without disrupting other consumers or requiring privileged access.

What a mock fundamentally lacks follows directly from what it is. A mock cannot execute business rules: there is no logic layer behind the response, only a lookup table matching requests to canned answers. It cannot persist writes, because there is no database recording what happened. Each of these limits traces back to the same root fact: a mock is a surface, not a system. Everything that follows in this piece, where mocks help and where they mislead, follows from that one structural fact.

The scenarios where mocks earn their place

Mocks are the right tool in four bounded, well-defined situations: when the backend does not exist yet, when the third-party API cannot be touched safely during iteration, when a specific failure mode needs to be forced on demand, and when team parallelism requires independence from another team's delivery schedule.

Parallel frontend and backend development is the clearest case. When the two sides meet at the integration seam, the rework needed is minimal because both were built to the same spec from the start. This works because nothing about the mock needs to be stateful for this purpose: the frontend only needs confirmation that the right shape of data arrives at the right endpoint, not confirmation that the backend's internal logic is correct.

Third-party APIs with rate limits or billing costs present a second case where mocking is the economically sound choice. Mocking the interface during that iteration avoids the rate-limit and billing cost.

Forced error injection is a third case, and arguably the one where mocks are least replaceable. Triggering a 401, a 429, or a 500 from a real backend on demand requires specific conditions that may be slow to arrange, risky to attempt against production infrastructure, or simply impossible to reproduce reliably. A mock delivers any of these in seconds, configured directly into the response profile. Testing how an application behaves during a full server outage is not something a real API can be made to do safely. That is a case where mocking isn't a workaround for a limitation, it is the only practical option available.

CI/CD pipeline stability is the fourth case. Unit and component tests running in CI need to be fast, deterministic, and independent of any external service's uptime. Mocks satisfy all three conditions at this layer; they remain the standard tool for this class of test.

Where the Stateless Design Hides Bugs

A stateless mock can confirm that code sends a request of the right shape and handles a response of the right shape. It cannot confirm that the system on the other end of that exchange actually behaves the way the test assumes it will, because the mock was never connected to a behaving system. That gap runs through every test suite built entirely on mocks, and it becomes visible when a workflow depends on something a single hardcoded response cannot represent: sequence.

Consider a multi-step flow. Any apparent consistency between those two responses is a property of how a developer configured the mock, not a property of any real API behavior. Workflows that depend on sequential state, an order that must exist before a fulfillment record can be created, a subscription that must be active before a charge is attempted, are structurally invisible to a stateless mock. The test can pass in full while the underlying assumption it was meant to verify goes completely unchecked.

A stateful simulator closes this gap by remembering previous requests: a POST creates a record, a subsequent GET returns that same record, a list call includes it, a PATCH updates it, and a state machine enforces which transitions are valid and which are not. That is a class of guarantee a stateless mock cannot provide, because of what a mock is built to do.

A second failure mode is drift. Because the mock has no connection to the live system, it doesn't register the change. The mock and the application built against it continue to agree on an assumption that the real world has already abandoned, and that disagreement becomes visible in production, usually at the worst possible time.

A third gap lies in everything a mock was never built to simulate. Real APIs provide server-side validation that catches edge cases no one thought to hardcode into a mock response. None of this makes the mock a flawed version of the real API. It makes the mock a different kind of tool, answering a narrower question than the one a full integration test needs answered.

The Mock-to-Real Boundary in a Layered Test Pipeline

The right architecture is a layered pipeline in which different stages answer different questions, and each stage uses the tool suited to the question it is asking, with mocks correct without qualification at the first layer.

At the first layer, unit and component tests, mocks are the correct tool without qualification. Tests at this layer need to be fast, deterministic, and runnable offline without credentials, and mocks satisfy all three conditions cleanly.

At the second layer, integration tests, something stateful becomes necessary. These tests validate end-to-end data flow, authentication behavior, business logic, and consistency across a sequence of steps, and a stateless mock cannot answer any of those questions because it has no memory of what happened in a previous call. Integration tests need to run against the real or staging API to exercise the full stack, or against a stateful, verified simulator that actually tracks state across calls the way the real system does.

At the third layer, end-to-end and smoke tests, full user workflows and cross-service interactions require real infrastructure. Mocks have no role at this stage. The entire point of end-to-end testing is to confirm that the full, real chain of systems behaves correctly together, and a hardcoded response defeats that purpose by design.

Stripe's own tooling illustrates this boundary clearly, directly from the provider itself. Stripe maintains stripe-mock, a mock server for validating that an SDK hits the right URL and sends the right parameters, to catch request and response shape errors. Stripe states that stripe-mock does not attempt to reproduce the behavior of the real API, that its responses are hardcoded, and that there are no plans to add statefulness to it. Stripe built this tool as a sanity-check for request and response shape, not as a substitute for integration testing against its real endpoints. That is the scope of a mock, defined by the company that owns the API it mocks, and it maps exactly onto the first layer of the pipeline described above. Nothing about that scope limit is a flaw in stripe-mock. It is an honest statement of what a stateless tool is for.

AI agents and multi-step tool-use expose the stateless ceiling faster

An AI agent that chains calls across multiple services turns the stateless mock's amnesia from a manageable blind spot into a critical failure mode. A realistic agent workflow can touch many services before it returns a single response to a user, an LLM, a tool server, one or more downstream SaaS APIs, and each hop in that chain needs to be simulatable in a way that preserves state across the entire sequence, not just within a single call.

Mocking the LLM API itself, the first level of this problem, is well served by stateless HTTP interception. This layer validates that the SDK parses model responses correctly, handles streaming output properly, and fires the correct tool calls in response to a given prompt. These are the same class of question a stateless mock answers well for any traditional piece of software: shape in, shape out, no memory required.

Mocking the downstream APIs an agent actually calls, the second level, is where statefulness stops being optional. If an agent's tool call creates a resource and a later tool call in the same workflow reads that resource back, a stateless mock returns its hardcoded read result regardless of whether the earlier create call actually happened. The test passes. The system underneath it remains unverified, because the mock was never capable of connecting the two calls. A stateful, verified simulator that tracks created resources, enforces valid state transitions, and fires webhooks when state changes is what makes an agent integration test meaningful.

Reinforcement learning and evaluation environments compound the problem further. Agents in these environments need to run thousands of episodes across horizontally scaled containers, and each episode requires a clean, consistent, reproducible starting state. A stateless mock cannot accumulate state even within a single episode, let alone reset cleanly between thousands of them. Neither tool, in its native form, satisfies what large-scale agent evaluation actually requires: a system that behaves like a real, stateful API, but one that can be spun up fresh, deterministically, over and over.

The verification gap: how a mock that was accurate becomes one that is not

A mock that was accurate on the day it was written becomes a liability the moment the real API changes underneath it and the mock does not change with it. Most teams have no mechanism in place to detect that moment when it arrives. The gap between the mock and reality can widen for months before anyone notices.

Mocks drift faster than generated simulators specifically because they are hand-maintained. Every change a provider makes to its API, a renamed field, a deprecated parameter, a different error code, requires a manual update on the mock's side. There is no automated path connecting "the real API changed" to "the mock now reflects that change." The update happens only when someone notices the discrepancy, which often means it happens after a production failure.

The principle that practitioners converge on to manage this risk is "mock early, validate late." But something, eventually, has to validate the mock's assumptions against the real API to confirm that the shortcut it provided was actually correct.

Three practices reduce the risk of drift in a durable way. First, generate mocks from a versioned API specification such as OpenAPI rather than writing them by hand, so that an update to the spec propagates automatically into the mock instead of waiting on a developer to notice and rewrite it. Third, use a simulator that is continuously and automatically verified against the live API before every release, so that fidelity is not an assumption carried forward from whenever the mock was last touched, but a published, auditable score that reflects the simulator's current accuracy against the real system. It replaces trust in a mock's age with evidence of its current correctness.

A decision framework for choosing between a stateless mock and a stateful simulator

The right question to ask is not whether to mock at all, but what a given test needs to prove, and whether a stateless response pattern is capable of proving it.

A stateless mock is the right choice when a test validates request shape, response parsing, or error-code handling, none of which require memory of a prior call. It is the right choice when the backend being tested against does not exist yet and both sides have agreed on a contract to build toward. It is the right choice when a test needs to run offline, without credentials, or inside a resource-constrained CI job. And it is the right choice when a specific, hard-to-trigger failure mode, a 429, a 503, a dropped connection, needs to be forced reliably and repeatedly on demand.

A stateful, verified simulator is the right choice when a test covers a multi-step workflow where the result of one call decides whether the next call is valid. It is the right choice when testing an AI agent that chains tool calls across an API, because the agent's behavior depends on what state that API is actually in, not merely on what shape of response comes back. It is the right choice when a team needs to run thousands of reproducible episodes in a reinforcement learning or evaluation environment at scale. And it is the right choice when confidence is needed that a simulator reflects the real API's current behavior, not an assumption that was accurate months ago and has quietly gone stale since.

For most teams, the answer is a layered assignment: stateless mocks doing the work at the unit and component layer, where they answer their questions accurately and cheaply, and stateful simulation doing the work at the integration layer, where sequence, state, and cumulative behavior matter. Neither tool replaces the other. Each one is built to answer a different question honestly, and a durable testing strategy is built by sending each question to the tool that can actually answer it.

More in Mocking vs. Stubbing