API tests as documentation — and why that's the only test suite anyone reads
Wiki pages about APIs go stale in three months. The API test suite gets opened every single day. If you want people to understand your API, write tests that read like documentation — and stop writing the wiki.
The asymmetry: who reads what, how often
Here's what actually happens to API documentation in most engineering teams.
The wiki page about the /users endpoint gets written when the feature ships. It includes the expected request shape, the response fields, the error codes, and a curl example. Developers opening it for the first time find it useful. Six months later, two fields have been renamed, a new optional parameter was added, and one of the documented error codes was removed when the validation logic changed. Nobody updated the wiki. The page is now partly accurate and partly misleading — and you can't tell which parts are which without reading the source code.
The test file for the /users endpoint gets opened every time a test fails, every time someone adds a new case, every time a new engineer is onboarding and trying to understand what the endpoint actually does. Unlike the wiki, the test file fails when it's wrong. A test that asserts res.body.user_id will fail the moment the field is renamed to res.body.id. The wiki won't notice.
This asymmetry — tests are live artifacts that fail on drift; docs are static artifacts that silently lie — means the test suite is almost always more accurate than the documentation. The question is whether it's also readable enough to replace it.
Three habits that turn a test suite into onboarding material
Write test names as questions-answered. The most impactful change you can make without touching the test logic itself: rename tests to describe the behaviour contract, not the test mechanics.
// Before: describes the test mechanics
test('test_user_not_found');
test('user endpoint 404');
// After: describes the contract
test('returns 404 when user does not exist');
test('returns the user object with id and email when user exists');
test('returns 401 when authorization header is missing');
test('returns 403 when user requests another user\'s profile');A new engineer reading the second set of test names understands the endpoint's behaviour contract in under a minute. They understand the auth model (a header is required), the access control model (users can only access their own profile), and the error vocabulary (401 for missing auth, 403 for insufficient auth). That's documentation — written as test expectations rather than prose.
Assert response shape, not just status. A test that only checks expect(response.status).toBe(200) documents less than one that asserts the response structure:
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
id: expect.any(String),
email: expect.any(String),
createdAt: expect.any(String),
role: expect.stringMatching(/^(admin|member|viewer)$/),
});The toMatchObject assertion is doing double duty: it's testing that the response has the right shape, and it's documenting that the response has those four fields with those types and constraints. A new engineer reading this knows the response schema without opening any external documentation. And when the schema changes, the test fails — so the documentation stays accurate.
Write failure output that explains the contract. When a test fails, the error message should explain what was expected and what arrived — not just that something went wrong. Test runners handle this reasonably well for expect() assertions. But the test name carries context the error output can't. "Expected 200, received 404" is less useful than "returns 200 when user exists — Expected 200, received 404." The first tells you there's a problem; the second tells you which aspect of the contract broke.
The practice nobody does: write names as answered questions
Most engineers write tests as statements: "user returns correct data," "invalid input returns error." The statement form works for testing, but it's weak documentation. You understand what the test checks only after reading the body.
Write tests as questions answered by the assertion. The implicit format: "[Given this state], [when this happens], [it returns this]." You don't need BDD syntax for this — just deliberate naming.
describe('GET /users/:id', () => {
describe('when the user exists', () => {
it('returns 200 with the user object', async () => { ... });
it('includes the user\'s role in the response', async () => { ... });
it('does not include the password hash in the response', async () => { ... });
});
describe('when the user does not exist', () => {
it('returns 404', async () => { ... });
it('returns an error message in the body', async () => { ... });
});
describe('when the authorization header is missing', () => {
it('returns 401', async () => { ... });
});
describe('when the user requests another user\'s profile', () => {
it('returns 403', async () => { ... });
});
});The describe/it nesting reads like a specification document. An engineer inheriting this codebase opens the test file and has a complete mental model of the endpoint's behaviour contract in under two minutes. They understand what fields the response includes and excludes, what every error code means, and what the access control policy is. That's what API documentation is supposed to produce. The tests just happen to be better at producing it, because they fail when they're wrong.
The counter-argument: tests are for testing
The pushback I hear most often: tests should test, docs should document. Mixing the two jobs means both are done poorly.
I don't find this compelling, and here's why: the reason tests tend to be better documentation than wiki pages is precisely because they have a forcing function. They run in CI. They fail when they drift from reality. A document written purely for documentation has no such check. Adding readability to tests doesn't compromise their testing function — it adds a second consumer (the engineer trying to understand the system) without removing the first (the CI runner verifying correctness).
The real failure mode is the opposite: over-investing in documentation-style names while neglecting coverage. A test suite that reads like an API spec but misses the error paths and edge cases is documentation that lies by omission. Readability and coverage compound; they don't trade off.
The realistic outcome: short README, readable tests, no wiki
Once I started treating API tests as the primary documentation artifact, the question became: what does the README actually need to cover?
Much less than before. The README needs the high-level orientation: what the service does, how to run it locally, where the tests are, and how to run them. It doesn't need to document individual endpoint response schemas, because the test files do that more accurately and stay accurate automatically.
The model that works in practice: a short README as overview, API test files as the detailed specification, no wiki pages covering individual endpoint behaviour. The choice of where those test files live matters here — a tool like Bruno that stores tests as plain-text .bru files in your repository makes them more readable in code review context than Postman's JSON exports.
Wiki pages about specific API endpoints are the artifact I'd eliminate first. They duplicate information the tests already contain, they're maintained less diligently (no CI failure to force an update), and they create a second source of truth that inevitably diverges. If the test file is readable enough to be the documentation — and with the habits above, it usually is — the wiki page is technical debt that compounds interest every time the API changes without a wiki update.
// related
Postman vs Insomnia vs Bruno in 2026: my pick for API testing
Three tools, three very different bets on what API testing should feel like. I've been comparing them for teams who want to move off ad-hoc curl scripts, and here's the pick.
Contract testing, explained without the Pact marketing
Contract testing is two things wearing one name: a model and a tool. The model is genuinely useful; the marketing for the tool oversells where it fits. Here's the model, separated from any vendor's pitch.