Skip to main content

testing

MANDATORY - Load before writing tests. Covers fakes over mocks, golden files, goleak, httptest, fuzzing, parallel tests, synctest, race detection, test coverage, table-driven tests. Enforces project test conventions. Use when this capability is needed.

Jump to install

Source facts

Repository
tomevault-io/skills-registry
Last source activity
April 28, 2026 at 22:53
Detected SKILL.md language
English
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions ยท Read-only preview
name
testing
description
MANDATORY - Load before writing tests. Covers fakes over mocks, golden files, goleak, httptest, fuzzing, parallel tests, synctest, race detection, test coverage, table-driven tests. Enforces project test conventions. Use when this capability is needed.
metadata
{"author":"air-gapped"}
# Go Testing Standards ## Fakes Over Mocks This is the 2025 consensus: prefer fakes over mocks. | Type | When to Use | |------|-------------| | **Real** | Always prefer if fast and reliable | | **Fake** | Dependency is slow/unreliable (DB, network, time) | | **Mock** | Only when verifying specific call sequences | ```go // Interface defined where USED, not where implemented type Clock interface { Now() time.Time After(d time.Duration) <-chan time.Time } // Fake with logic (not predetermined responses) type FakeClock struct { current time.Time } func (c *FakeClock) Now() time.Time { return c.current } func (c *FakeClock) Advance(d time.Duration) { c.current = c.current.Add(d) } ``` **Key rules:** - Define interfaces where they are **used**, not where implemented - Keep interfaces small (1-3 methods) - Fakes have logic; mocks have canned responses Sources: [Learn Go with Tests](https://quii.gitbook.io/learn-go-with-tests/testing-fundamentals/working-without-mocks), [Leapcell: gomock vs Fakes](https://leapcell.io/blog/mastering-mocking-in-go-gomock-vs-interface-based-fakes) --- ## Testing Time-Dependent Code Cache TTLs, timeouts โ€” all need deterministic time. ### Option 1: Interface Injection (benbjohnson/clock) ```go import "github.com/benbjohnson/clock" type Cache struct { clock clock.Clock } // Production cache := &Cache{clock: clock.New()} // Test mock := clock.NewMock() cache := &Cache{clock: mock} mock.Add(5 * time.Minute) // Advance time programmatically ``` ### Option 2: testing/synctest (Go 1.25+) Runs code in a "bubble" with fake time that auto-advances when goroutines block: ```go import "testing/synctest" func TestTimeout(t *testing.T) { synctest.Test(t, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Time advances instantly when all goroutines are blocked <-ctx.Done() // Returns immediately, fake clock jumped 5s }) } ``` **Note:** Go 1.24 requires `GOEXPERIMENT=synctest`. Go 1.25 includes it in stdlib. Sources: [Go Blog: Testing Time](https://go.dev/blog/testing-time), [benbjohnson/clock](https://github.com/benbjohnson/clock), [Go Blog: synctest](https://go.dev/blog/synctest) --- ## Testing HTML Handlers with httptest The primary integration testing pattern for cooked. Use `httptest.NewServer` for full end-to-end tests and `httptest.NewRecorder` for unit-testing individual handlers. ### Full Server Test ```go func TestRenderMarkdown(t *testing.T) { // Set up a fake upstream that serves raw markdown upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("# Hello\n\nWorld")) })) defer upstream.Close() // Start cooked with the fake upstream srv := httptest.NewServer(cookedHandler(deps)) defer srv.Close() resp, err := http.Get(srv.URL + "/" + upstream.URL + "/README.md") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("got status %d, want 200", resp.StatusCode) } if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { t.Fatalf("got content-type %q, want text/html", ct) } } ``` ### Checking Response Headers ```go func TestResponseHeaders(t *testing.T) { // ...setup... resp, _ := http.Get(srv.URL + "/" + upstream.URL + "/file.md") // Verify X-Cooked-* headers if got := resp.Header.Get("X-Cooked-Cache"); got == "" { t.Error("missing X-Cooked-Cache header") } if got := resp.Header.Get("X-Cooked-Content-Type"); got != "markdown" { t.Errorf("X-Cooked-Content-Type = %q, want markdown", got) } } ``` --- ## Goroutine Leak Detection (goleak) Detects goroutines that outlive tests โ€” critical for long-running services. ### Package-Wide (Recommended) ```go // main_test.go func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ``` ### Per-Test (When Debugging) ```go func TestSpecificLeak(t *testing.T) { defer goleak.VerifyNone(t) // test code } ``` ### Ignoring Known Leaks ```go func TestMain(m *testing.M) { goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop"), ) } ``` **Note:** goleak doesn't work well with `t.Parallel()` โ€” use `VerifyTestMain` instead. Sources: [uber-go/goleak](https://github.com/uber-go/goleak), [Brandur: Goroutine Leaks](https://brandur.org/fragments/goroutine-leaks) --- ## Fuzzing Use for URL parsing, markdown preprocessing, HTML sanitization, and security-sensitive input handling. **File organization:** Fuzz tests go in separate `*_fuzz_test.go` files, not mixed with unit tests. ``` url.go url_test.go # Unit tests url_fuzz_test.go # Fuzz tests ``` ```go func FuzzParseUpstreamURL(f *testing.F) { // Seed corpus f.Add("https://example.com/file.md") f.Add("http://cgit.internal/repo/plain/README.md") f.Add("") f.Add("not-a-url") f.Fuzz(func(t *testing.T, input string) { _, _ = ParseUpstreamURL(input) // Should not panic }) } ``` ### Running ```bash go test -fuzz=FuzzParseUpstreamURL -fuzztime=30s ./internal/... # Clear cache when large go clean -fuzzcache ``` Sources: [Go Fuzzing Docs](https://go.dev/doc/security/fuzz/) --- ## Golden Files The primary testing pattern for HTML output. Keep in `testdata/golden/`. ```go var update = flag.Bool("update", false, "update golden files") func TestRenderOutput(t *testing.T) { got := renderMarkdown(input) golden := filepath.Join("testdata", "golden", t.Name()+".html") if *update { os.WriteFile(golden, got, 0644) return } want, _ := os.ReadFile(golden) if !bytes.Equal(got, want) { t.Errorf("mismatch; run with -update to regenerate") } } ``` Run `go test -update` to regenerate. Always commit golden files. --- ## Parallel Tests ```go for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // test }) } ``` **Critical:** Use `t.Cleanup()` not `defer` โ€” defer runs when the outer function returns, before parallel subtests complete. ```go // Wrong - cleanup happens too early t.Run("test", func(t *testing.T) { t.Parallel() defer cleanup() // Runs immediately! }) // Correct t.Run("test", func(t *testing.T) { t.Parallel() t.Cleanup(cleanup) // Runs after subtest completes }) ``` Sources: [Parallel Table-Driven Tests](https://www.glukhov.org/post/2025/12/parallel-table-driven-tests-in-go/) --- ## Makefile Targets Use Makefile targets, not bare `go test`: ```bash make test # All tests make test-race # With race detector ``` ## Key Files - `internal/*_test.go` โ€” Package tests - `testdata/golden/` โ€” Golden HTML output files - `testdata/fixtures/` โ€” Input test fixtures (markdown, code files) --- > Converted and distributed by [TomeVault](https://tomevault.io/claim/air-gapped) โ€” claim your Tome and manage your conversions. <!-- tomevault:4.0:skill_md:2026-04-13 -->
View on GitHub