DEV Community

Cover image for Your Mock API Has Amnesia: It’s Hiding Workflow Bugs
Ankit Jain
Ankit Jain

Posted on

Your Mock API Has Amnesia: It’s Hiding Workflow Bugs

Your test creates an order, updates it, and fetches it. Every request gets a plausible JSON response. If the test checks only those response bodies, it can pass even when the client never sends the update.

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.

Model state transitions

Consider an export API. A client starts an export, polls its status, and downloads a result only after completion.

absent → queued → running → completed
                    └────→ failed
Enter fullscreen mode Exit fullscreen mode

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.

Build a stateful fixture

Beeceptor provides counters, a key-value data store, and lists for state across calls. Enable response templating on each rule that uses 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')}}}
}
Enter fullscreen mode Exit fullscreen mode

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')}}}
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.

Isolate and reset state

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.

Test 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.

Keep the model small, version it alongside the contract, and assert each transition through a subsequent read. A workflow test earns your team’s confidence when it proves that the client sent the write and the dependency retained the resulting state.

Top comments (0)