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.

跳到安装

来源信息

仓库
tomevault-io/skills-registry
最近来源活动
2026年4月28日 22:53
检测到的 SKILL.md 语言
英语
星标
1
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
2 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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 -->
在 GitHub 查看