# test double: a worked example

Canonical page: https://vibe-coding-101.brighteng.org/c/test-double/#before-and-after

## The problem

A product screen loads a price from an external service. You need to check its timeout state without waiting for the real service to become unavailable. Keep the screen logic real and substitute only the price-loading boundary.

## Prompt to paste

Use a test double at the external service boundary to simulate a timeout. Assert recovery, and keep a separate test of the real integration.

## Before

The happy path returns a price, but a rejected dependency escapes without a defined screen state. A happy-path check alone leaves that missing behavior undiscovered.

## After

Use a controlled loader that rejects with TimeoutError to expose the failure. Handle that specified error as an unavailable state, preserve the success path, and let unexpected errors propagate instead of hiding them.

## Run the check

Save the following code as test-double.mjs and run `node --test test-double.mjs` with Node.js 22.12 or later.

```js
import test from 'node:test';
import assert from 'node:assert/strict';

async function before(loadPrice) {
  return { status: 'ready', price: await loadPrice() };
}

async function after(loadPrice) {
  try {
    return { status: 'ready', price: await loadPrice() };
  } catch (error) {
    if (error.name !== 'TimeoutError') throw error;
    return { status: 'unavailable', message: 'Price unavailable. Try again.' };
  }
}

const timeout = async () => {
  throw new DOMException('Price service timed out', 'TimeoutError');
};

test('a controlled timeout exposes missing recovery', async () => {
  assert.deepEqual(await before(async () => 100), { status: 'ready', price: 100 });
  await assert.rejects(before(timeout), { name: 'TimeoutError' });
});

test('the real handler recovers and still handles success and unexpected errors', async () => {
  let calls = 0;
  const unavailable = await after(async () => { calls++; return timeout(); });
  assert.deepEqual(unavailable,
    { status: 'unavailable', message: 'Price unavailable. Try again.' });
  assert.equal(calls, 1);
  assert.deepEqual(await after(async () => 100), { status: 'ready', price: 100 });
  await assert.rejects(after(async () => { throw new Error('invalid response'); }),
    /invalid response/);
});
```

## Expected result

The controlled timeout makes the initial handler reject. The revised handler returns the specified unavailable state, still returns a real price from the success stub, and propagates an unexpected failure. The test double replaces the loader, not the handler being checked.

## Limits

A test double is a substitute used in a test; it does not make the application resilient by itself. This stub assumes the boundary uses TimeoutError. Match the real client contract and keep separate integration checks for request shape, decoding, authentication, and actual error types. A stub that returns the desired final screen state without running the handler would not test recovery.

## Sources

- [Martin Fowler: Test Double](https://martinfowler.com/bliki/TestDouble.html): Test double as the umbrella term and stubs as controlled canned responses.
- [MDN: AbortSignal timeout](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal#static_methods): TimeoutError is used by AbortSignal.timeout; the example assumes that explicit boundary contract.

Sources checked: 2026-10-11. These are original illustrative examples with automated checks. Human editorial approval is not recorded. Policy: https://vibe-coding-101.brighteng.org/editorial/
