Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Deterministic Test Doubles and Fault Injection

Last updated: 5 Oct 20266 min read
tutorial
IntermediateBy AITrove Editorial

A test double replaces a dependency for one check. It is useful when a remote service is slow, costly, or hard to make fail on command. The risk is a double that always returns a convenient success and hides real status, timing, or payload behavior. Define the observed contract first, then make the double expose the same meaningful states: success, rejection, timeout, retryable failure, and duplicate delivery where relevant. Use a deferred response to control order in race tests. Keep a separate contract check against the real boundary so the double does not become an alternate specification.

Working case

A browser route fetches case 47, then case 62. In normal development both responses finish quickly, so the stale-result bug rarely appears. A deferred transport can resolve 62 first, then 47, every time. The assertion should check that the panel stays on 62 and that an old error cannot replace the current route's state. For a webhook, a different double can deliver the same event twice and check idempotency. The shared idea is controlled uncertainty; the exact script depends on the contract being tested.

Implementation boundary

javascript
function deferredResponse() {
  let release;
  const promise = new Promise(resolve => { release = resolve; });
  return { promise, release };
}
const case47 = deferredResponse();
const case62 = deferredResponse();
case62.release({ caseId: 62 });
case47.release({ caseId: 47 });
console.log((await case62.promise).caseId);
// Output: 62

The deferred helper controls completion order and can be extended with reject and abort paths. Do not insert arbitrary sleeps into a race test: fixed delays are slow and can still fail unpredictably on a loaded machine. Assert after both promises settle and make the current route identity part of the observed state. For HTTP integrations, record real status and payload expectations in contract fixtures and refresh them under review when a provider changes. Keep one environment-level smoke check for authentication, headers, and network behavior that a local double cannot cover.

Cost and boundaries

A deferred helper has constant setup work and uses memory for outstanding promises until they settle. More elaborate doubles cost maintenance: every simulated state is another behavior to keep aligned with reality. Fault injection in a test database or controlled staging service can give stronger evidence but costs setup and cleanup. Use the simplest control that exposes the target failure. A long suite full of sleeps and random retries spends time without making failures easier to reproduce or explain.

Failure trace

A route test mocks fetch with an immediate resolved object, so it never sees old request completion. In production, a slow case 47 response overwrites case 62. The team adds a hundred-millisecond sleep but the test is flaky because completion order still varies. Replace the sleep with explicit deferred responses, run both orders, and fail the old request after the route changes. Also test cleanup on unmount. Keep a real API contract check so the artificial response shape cannot drift unnoticed.

Verification

  • Resolve current and obsolete requests in both orders without sleeps.
  • Reject the obsolete request and confirm it cannot replace the current error or success state.
  • Compare double payloads and statuses with a real API boundary check.

Practice drill

Write a transport double with named controls for resolve, reject, and abort. Start requests for cases 47 and 62, settle them in both orders, and record the visible case ID and error state after each step. Inject a timeout for the current request and an error for the obsolete one. Then run one request through the actual HTTP handler with raw JSON to verify that the double's fields and status assumptions still match the service.

Decision note

Use deterministic doubles for timing and rare failures, and pair them with a separate check of the real integration contract.

Common Mistakes

  • Using arbitrary delays to provoke races.
  • Mocking away every integration check with invented success objects.
  • Testing only the success callback while an obsolete error path remains active.

Connected lessons

Quality and Capacity Engineering; Test Boundaries and Evidence Selection; Accessibility and Visual Regression Checks; Load Tests and Capacity Budgets; React Effects and Request Races; Tests across boundaries: assert behavior, not implementation text; Idempotent Write Requests and Lost Responses.

Apply and check

Build Project: release evidence and capacity and review Web Development: search and quality contracts quiz.

web-tech
web-development
Storage details