# 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.

- Skill: `tomevault-io/testing-16` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/testing-16`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/testing-16/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/testing-16

---


# 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 -->

