| title | Test pyramid |
|---|---|
| slug | tests |
| summary | Four-layer test pyramid (unit, contract, integration, e2e) and the rule that every function ships with a dedicated unit test. Included from the Go and TypeScript architecture pages. |
mdsmith follows a four-layer test pyramid. Each layer answers a different question and sits in a different place in the tree:
- Unit — one function or method per test. Lives next to source. No file I/O beyond inline string fixtures. Runs in milliseconds.
- Contract — locks a port-package interface or external surface shape. A contract test must fail loudly when the shape it pins drifts.
- Integration — multiple packages composed together against real Markdown fixtures.
- E2E — the built binary (or the
packaged extension) against a
fixture workspace. Website JS is
covered by
website/e2e/.
The pyramid shape — many unit, fewer contract, fewer integration, fewest e2e — keeps the suite fast and the feedback loop tight.
A new function lands together with
its dedicated unit test by name.
Sub-behaviours of the same function
go in subtests under that parent.
The rule applies to exported and
unexported functions alike — in
production code. Test files
(*_test.go, *.test.ts) and
test-only helpers are out of scope:
the audit walks production sources
only and never asks for "tests for
tests". The audit flags every
production function in the touched
set that lacks a matching test.
The language-specific page binds
this rule to concrete file and
symbol patterns. For Go, that is
TestFunctionName for package
functions and TestReceiver_Method
for methods. For TypeScript, that
is a describe("name") block with
one or more test("case") cases
imported from bun:test.
A production function may skip its dedicated test only if one of these holds:
- It is generated code (file begins
with a
// Code generated…header, matches a generator file pattern such as*_gen.go, is a*.d.tsdeclaration, or is emitted underdist/). The file-level marker is sufficient — no per-function comment is required. - It is a trivial accessor with no
branch — a one-line getter or a
String()-style format method. Add a one-line comment on the function so the audit can distinguish "no test by design" from "no test, forgotten".
A unit test on the same behaviour is faster than the equivalent integration test. It stays focused on one function. It also survives refactors better. The audit pushes back on inverted pyramids:
- An integration test that exercises one function should move down to that function's own package as a unit test.
- An e2e test that exercises behaviour reachable through the integration layer should move down to integration.
Save e2e for the full process boundary. Use it for exit codes. Use it for signals. Use it for subprocess lifecycle. Use it for packaged-artifact tests.