4e433158cd
---ci--- project: atelier phase: 3 milestone: v0.4 status: complete phase_role: execution phase_tag: v0.3.3 requirements: covered: [ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105] partial: [] ---/ci---
6.7 KiB
6.7 KiB
Go Testing — Derived Application
Applies Atelier's domain principles to Go testing specifically. Derives from
domains/docs; introduces no new P-rules (D-063). Seelanguages/go.mdfor the language first-principles stub.
Table-Driven Tests (Testing P1 Tests as Specification, C2 Clarity)
- Table-driven is the Go idiom:
cases := []struct{ name string; in X; want Y }{...}; loop witht.Run(c.name, ...). Each case is a subtest with its own name and failure output. - Test names read as a spec:
{"rejects empty email", ...},{"returns persisted id", ...}. A reader understands the unit from the subtest names (Testing P1). - No
if got != want { t.Fatal() }shared across cases: each case asserts independently; a failure in case 3 does not skip cases 4 and 5. t.Runenables-runfiltering:go test -run TestCreateUser/rejects_empty_emailruns one case. Essential for debugging a single failure.
func TestCreateUser(t *testing.T) {
cases := []struct {
name string
email string
wantErr bool
}{
{"rejects empty email", "", true},
{"rejects missing @", "no-at-sign", true},
{"accepts valid email", "a@b.co", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, err := CreateUser(c.email)
if (err != nil) != c.wantErr {
t.Fatalf("err=%v, wantErr=%v", err, c.wantErr)
}
})
}
}
t.Parallel for Independence (Testing P2 Independence, Concurrency P10 Test for Race Conditions)
t.Parallel()for independent subtests: each subtest opts in; the runner executes them concurrently. A test that fails underParallelhas hidden state (Testing P2 Independence).- Capture loop variables:
c := cinside the loop, or rely on Go 1.22+ per-iteration scoping. A parallel subtest sharingcraces on the last value. - Applies
concurrency/P10(test for races): parallel tests are the first line of race detection; combine with-racefor the full safety net.
for _, c := range cases {
c := c // capture for parallel
t.Run(c.name, func(t *testing.T) {
t.Parallel()
_, err := CreateUser(c.email)
if (err != nil) != c.wantErr {
t.Fatalf("err=%v, wantErr=%v", err, c.wantErr)
}
})
}
t.Cleanup for Teardown (Testing P3 Determinism, Testing P2 Independence)
t.Cleanup(func() { ... })for teardown: runs in LIFO order after the test (and its subtests) complete. Replacesdeferin a helper that does not know when the test ends.- Per-test state, not shared: a
setup(t)helper creates resources and registers cleanup; each test gets its own. A package-levelvarshared across tests is order coupling. t.TempDir()for filesystem tests: creates a unique temp dir and cleans up automatically. No manualos.RemoveAlland no cross-test contamination.- Applies
Testing P3(determinism): cleanup is tied to the test lifecycle, not a global teardown that may run before or after depending on order.
func setupStore(t *testing.T) *Store {
t.Parallel()
dir := t.TempDir() // auto-cleaned
s, err := OpenStore(filepath.Join(dir, "db"))
if err != nil { t.Fatal(err) }
t.Cleanup(func() { s.Close() })
return s
}
Race Detector (Testing P9 Edge Case Coverage, Concurrency P6 No Silent Races)
go test -racein CI, always: seego-tooling.md. The detector is the enforcement ofconcurrency/P6.- Tests must exercise the concurrent path: a serial test of a
Mutex-protected map finds no race. Write tests with N goroutines. -count=1to disable result caching: by default, Go caches passing tests.-count=1forces re-run; combine with-raceand parallelism to surface heisenbugs.- Applies
Testing P9(edge case coverage): the race detector is the edge-case tool for concurrency — it finds the inputs the test author forgot to write.
# CI gate
go test -race -count=1 ./...
Time and Determinism (Testing P3 Determinism, Testing P9 Edge Case Coverage)
- No
time.Now()in code under test: inject aClockinterface. In tests, a fake clock advances deterministically. time.Sleepin tests is a smell: a sleep waits for a real timer, flaky under load. Use a channel orEventually-style polling with a timeout.t.Deadline()aware helpers: a helper that may take long checkst.Deadline()and bails early. Prevents a slow test from timing out the suite.
type Clock interface { Now() time.Time }
type fakeClock struct{ t time.Time }
func (f *fakeClock) Now() time.Time { return f.t }
func TestUserHasCreatedAt(t *testing.T) {
clk := &fakeClock{time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}
u, _ := CreateUserWithClock("a@b.co", clk)
if u.CreatedAt.Year() != 2024 {
t.Fatalf("year=%d, want 2024", u.CreatedAt.Year())
}
}
Mocks and Interfaces (Testing P7 Realism, API P1 Contract Fidelity)
- Mock at the interface, not the struct:
type Store interface { Get(id string) (*User, error) }in production;type mockStore struct{ ... }in test. The interface is the contract (appliesapi/P1). httptestfor HTTP servers:httptest.NewServergives a real server on a loopback port; no manual socket plumbing.testify/mockor hand-written mocks: hand-written for one-off,testifyfor complex sequencing. Avoid mocking frameworks that generate code at runtime (reflection-heavy) — they hide failures behind stack traces.- Applies
Testing P7(realism): mock the boundary (HTTP, DB), not the unit. Mocking the unit under test tests the mock.
type mockStore struct {
users map[string]*User
got []string
}
func (m *mockStore) Get(id string) (*User, error) {
m.got = append(m.got, id)
return m.users[id], nil
}
func TestGetUserLogs(t *testing.T) {
s := &mockStore{users: map[string]*User{"abc": {}}}
svc := NewService(s)
svc.GetUser("abc")
if len(s.got) != 1 || s.got[0] != "abc" {
t.Fatalf("got=%v", s.got)
}
}
Cross-References
domains/testing/pyramid.md— where unit/integration/race tests sit; the race job is its own layer.domains/testing/fixtures.md—t.TempDirandt.Cleanupas the fixture discipline.domains/testing/first-principles.md— Testing P1 Specification, P2 Independence, P3 Determinism, P9 Edge Coverage.domains/concurrency/first-principles.md— Concurrency P6 (race detector), P10 (test for races).languages/go-types.md— the named types tests assert.languages/go-concurrency.md— concurrent tests exercise the patterns from that doc.languages/go-tooling.md— thego testflags (-race,-count,-run) detailed here.