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:
- Runtime contract functions accept or reject unknown values.
getQuestionsturns options into an HTTP request and a validated result.- The SDK and API communicate through real
RequestandResponseobjects. - 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
- Write one test that describes missing behavior.
- Run it and confirm it fails for the expected reason.
- Write the smallest implementation that passes.
- Run the focused test, then the full suite.
- 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
- Valid and invalid values for every closed union and numeric boundary.
- Answer-free responses reject answer fields.
- Answer-bearing calls return answer-bearing types and values.
- Malformed success JSON is rejected.
- Typed API errors remain inspectable.
- Network failures preserve their cause.
- SDK and API agree in an integration test.
- Documented imports compile from the package entry point.
Check your understanding
Why is replacing
fetchacceptable while mocking an internal URL builder is not?Show answer
fetchis the external network seam. A URL builder is an implementation detail. The public test should observe the request passed tofetch, not how internal code constructed it.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.