Tests exist to make change safe. A test that only restates the implementation is a liability: it slows refactoring without catching defects. Test behaviour through the public surface of a package.
| Level | Scope | Tooling | Where |
|---|---|---|---|
| Unit | one package, no I/O | go test, table-driven |
*_test.go beside the code |
| Integration | several packages, real filesystem or DB | go test with build tag integration |
*_test.go, //go:build integration |
| End-to-end | real binary, real HTTP | Playwright / shell harness | e2e/ |
| Frontend unit | component and hook logic | Vitest | web/**/*.test.tsx |
Unit tests dominate. Integration and E2E tests are reserved for behaviour that genuinely crosses a boundary and would be misrepresented by mocks.
Every PR runs, and must pass:
go vet ./...
gofmt -l .
go test -race -cover ./...
govulncheck ./...
Integration tests run when the change touches the code they cover:
go test -tags=integration ./...
t.Run, so failures name the case.time.Sleep to synchronise, no
dependence on wall-clock ordering, no shared state between cases. Use
t.Cleanup, t.TempDir, t.Setenv, and injectable clocks.internal/testutil, are imported only from
_test.go files, and are blocked from production builds by depguard.Any code path that shares state across goroutines ships with a test run under
-race. A green non-race run proves nothing about a concurrent defect.
A change with a performance claim includes a benchmark:
go test -bench=. -benchmem ./path/to/pkg
make bench records before/after numbers so reviewers can check the claim
rather than trust it.