Your Mock API Has Amnesia—and It’s Hiding Workflow Bugs
Your test creates an order, updates it, and fetches it. Every request gets a plausible JSON response. The test passes even if the update never happened. That is the blind spot of independent static fixtures: they can de
Your test creates an order, updates it, and fetches it. Every request gets a plausible JSON response. The test passes even if the update never happened.
That is the blind spot of independent static fixtures: they can describe valid snapshots without preserving a valid workflow. API virtualization becomes much more useful when the substitute remembers what happened before.
For QA engineers and SDETs, a stateful virtual API can exercise client journeys without depending on a shared database. For developers and solutions architects, it makes the workflow assumptions visible enough to challenge.
1. Model state transitions before writing response bodies
Consider an export API. A client starts an export, polls its status, and downloads a result only after completion.
absent → queued → running → completed
└────→ failed
Define the externally visible contract for each transition:
| Operation | Preconditions | Observable result |
|---|---|---|
| Start export | Valid request | Job ID and initial status |
| Poll export | Known job ID | Current status for that job |
| Download | Job completed | Result payload |
| Download early | Job queued or running | Contract-defined error |
| Poll unknown job | ID absent | Contract-defined not-found response |
Do not invent status codes during implementation. Use your API's contract. A 409 may represent an early download in one API; another may return 202 with a status link.
Choose how transitions advance. A poll counter is deterministic and useful for testing client polling behavior. A clock-driven transition exercises elapsed-time behavior. A test-controlled transition provides precise setup. These models answer different questions: polling three times is not equivalent to waiting thirty seconds.
2. Build a small stateful fixture, then assert persistence
Beeceptor provides counters, a key-value data store, and lists for state across calls. Response templating must be enabled on the rules using these helpers. Stateful mock documentation.
Start with a deliberately small, single-order fixture to learn the mechanics. The following templates accept a numeric amount and a simple string orderId. They are a teaching fixture, not a general order service.
For a POST /orders rule, select status 201, set Content-Type: application/json, enable templating, and use:
{{data-store 'set' 'orderId' (body 'orderId')}}
{{data-store 'set' 'orderAmount' (body 'amount')}}
{{data-store 'set' 'orderStatus' 'created'}}
{
"orderId": {{{json (data-store 'get' 'orderId')}}},
"amount": {{{json (data-store 'get' 'orderAmount')}}},
"status": {{{json (data-store 'get' 'orderStatus')}}}
}
For a GET /orders/latest rule, select status 200 and use:
{
"orderId": {{{json (data-store 'get' 'orderId')}}},
"amount": {{{json (data-store 'get' 'orderAmount')}}},
"status": {{{json (data-store 'get' 'orderStatus')}}}
}
The JSON helper serializes stored values; triple braces emit that serialized JSON without HTML escaping. Create the order before reading it. Add explicit rules for missing or invalid inputs before expanding this example.
Now send requests to the endpoint URL copied from your dashboard:
export MOCK_BASE='https://YOUR-ENDPOINT.free.beeceptor.com'
curl --fail-with-body "$MOCK_BASE/orders" \
-H 'Content-Type: application/json' \
--data '{"orderId":"order-qa-17","amount":1250}'
curl --fail-with-body "$MOCK_BASE/orders/latest"
Assert that the second response contains the ID and numeric amount from the first request. Then add an update rule and assert the changed status on a subsequent read. That final read is what catches a client that displays optimistic state without actually sending the write.
This fixture has one storage slot. A second order overwrites the first. To test multiple orders, model storage by resource identity or use CRUD routes designed for that purpose. Do not mistake a latest endpoint demonstration for multi-entity persistence.
3. Treat isolation and reset as part of the API model
Stateful mocks introduce the same question as a database: who owns the state?
Two parallel workers using the keys above can overwrite each other's orders. If one resets a shared polling counter while another is waiting for completion, both tests become misleading.
Use one of these explicit strategies:
- An isolated virtual endpoint per worker or test run.
- Namespaced state keys that include the run identity and resource identity, with matching rules that consistently use that namespace.
- Serial execution for a deliberately shared fixture, with deterministic initialization before every case.
A unique header only isolates state if the fixture actually uses it when selecting keys or rules. Merely adding X-Test-Run to requests does not change shared storage.
Reset state in setup so a previous crash cannot poison the next run. Also clean up after completion, but do not make correctness depend solely on teardown. Keep test-control endpoints out of the application's normal API surface.
For each failing case, preserve the initial state, request sequence, and final state. Avoid logging secrets from payloads. “Passed on retry” is particularly suspicious here: it may mean the first attempt accidentally prepared the fixture for the second.
4. Use stateful virtualization to challenge workflow assumptions
Once the basic path works, test behaviors that static JSON rarely exposes:
- A poll returns the same intermediate status several times.
- A job fails after the client has already shown progress.
- A repeated create request refers to the same logical operation.
- A read observes an older state until a test-controlled transition occurs.
- A terminal result remains terminal when the client repeats a request.
Stateful storage alone does not guarantee atomic updates, transaction isolation, or idempotency under concurrent requests. A sequential mock can demonstrate intended client behavior without proving that the real server's concurrent implementation is correct. Test those properties against the actual backend too.
The useful artifact is a small executable model of the dependency's public behavior. Keep it versioned alongside the contract, and make each transition earn its place through an assertion. If your test says a workflow succeeded, require evidence that the workflow's state actually changed.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.