Recipe 04 / Test batch

Taste the failure cases, too.

This batch tests the JSON client from the previous recipe using the built-in Node.js test runner. Each response is supplied locally, so no live service or credential is involved.

Result: Three focused behavior checksGo to method
  1. Set out the behavior

    The client promises to return parsed JSON for a successful response and reject an unsuccessful response or invalid body. Write checks against those visible results. Avoid asserting the function's internal variable names or the exact order of harmless implementation steps; those details should be free to change during a refactor.

  2. Control the ingredients

    The fake fetch functions below return standard Response objects. Each test gets its own response because a response body is consumed when read. The reserved example.invalid address documents that this is a fixture; it is never contacted because fetchImpl is supplied. Save the test beside client.mjs and run the command shown below.

  3. Use failures as information

    Expect all three checks to pass for the client in the API recipe. If one fails, inspect the assertion and actual error before changing code. These checks do not cover timeout behavior, authentication, network redirects, or your application's response schema. Add focused cases for those concerns when you introduce them rather than treating three passing tests as complete coverage.

client.test.mjs

import test from 'node:test';
import assert from 'node:assert/strict';
import { getJson } from './client.mjs';

const url = 'https://example.invalid/data';

test('returns parsed JSON', async () => {
  const result = await getJson(url, {
    fetchImpl: async () => new Response('{"count":2}', { status: 200 })
  });
  assert.deepEqual(result, { count: 2 });
});

test('rejects unsuccessful HTTP status', async () => {
  await assert.rejects(getJson(url, {
    fetchImpl: async () => new Response('Unavailable', { status: 503 })
  }), /HTTP 503/);
});

test('rejects malformed JSON', async () => {
  await assert.rejects(getJson(url, {
    fetchImpl: async () => new Response('not json', { status: 200 })
  }), SyntaxError);
});

Expected result

Expected result: 3 passing tests, 0 failing tests.

Run the recipe

node --test client.test.mjs

The serving check

All three tests pass without any network request. The tests still fail if the client stops rejecting HTTP errors or malformed JSON.

Next at the counterDebug a recipe