Test through public boundaries

Tests should prove what consumers observe and survive internal refactors.

Agree on the seams before writing tests

A seam is a public boundary where a test supplies input and observes output. This SDK has four useful seams:

  1. Runtime contract functions accept or reject unknown values.
  2. getQuestions turns options into an HTTP request and a validated result.
  3. The SDK and API communicate through real Request and Response objects.
  4. A TypeScript consumer can import the documented types and gets the promised inference.

Do not test private URL helpers or assert that one internal module called another. Those tests break when structure changes even if users see identical behavior.

Work in small red and green slices

  1. Write one test that describes missing behavior.
  2. Run it and confirm it fails for the expected reason.
  3. Write the smallest implementation that passes.
  4. Run the focused test, then the full suite.
  5. Review names and duplication after behavior is green.

A failing test is evidence only when you saw the failure. A test added after implementation may pass without proving it could catch the bug.

Specify runtime contract behavior

import { type } from "arktype";
import { describe, expect, it } from "vitest";
import { GetQuestionsInput } from "./index.js";

describe("GetQuestionsInput", () => {
  it("rejects a fractional count", () => {
    const result = GetQuestionsInput({
      category: "science",
      count: 1.5,
    });

    expect(result).toBeInstanceOf(type.errors);
  });
});

The literal 1.5 comes from the rule that count must be an integer. The test does not reproduce the schema algorithm.

Mock the network boundary, not your own modules

it("rejects a malformed API response", async () => {
  const fetchMock = vi.fn<typeof fetch>(async () =>
    Response.json({ questions: "not-an-array" }),
  );

  const result = getQuestions(
    { category: "science", count: 2 },
    { baseUrl: "https://quiz.example", fetch: fetchMock },
  );

  await expect(result).rejects.toThrow(
    "Invalid quiz API response",
  );
});

fetch is an external boundary, so replacing it is appropriate. The test calls the real SDK function and the real runtime schema.

Connect SDK and API in memory

it("gets questions through the HTTP contract", async () => {
  const apiFetch: typeof fetch = async (input, init) =>
    handleRequest(new Request(input, init));

  const output = await getQuestions(
    {
      category: "science",
      count: 1,
      includeAnswer: true,
    },
    { baseUrl: "https://quiz.example", fetch: apiFetch },
  );

  expect(output.questions[0]?.correctChoiceIndex).toBe(1);
});

No network port is needed. Real web-platform objects preserve URL parsing, status codes, headers, and JSON behavior while keeping the test fast.

Compile consumer promises

declare const config: QuizSdkConfig;

const output = await getQuestions(
  {
    category: "science",
    count: 1,
    includeAnswer: true,
  },
  config,
);

output.questions[0]?.correctChoiceIndex;

Include this file in pnpm typecheck. If an overload or re-export breaks, TypeScript fails before publication. Runtime tests cannot prove editor inference.

A useful minimum release suite

Check your understanding

  1. Why is replacing fetch acceptable while mocking an internal URL builder is not?

    Show answer

    fetch is the external network seam. A URL builder is an implementation detail. The public test should observe the request passed to fetch, not how internal code constructed it.

  2. What does a type test catch that a runtime test cannot?

    Show answer

    It catches broken exports, declarations, overload selection, and inference seen by TypeScript consumers.