Advanced Go testing — interface-based mocking, benchmarks (basic, size-parametrized, allocation), fuzzing (Go 1.18+), test coverage, HTTP handler testing with httptest, best practices, and CI/CD integration. Extends go-testing.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
This skill extends go-testing with mocking, benchmarks, fuzzing, HTTP testing, and CI/CD.
net/http/httptestbenchstat to detect performance regressionsThe idiomatic Go approach: define a minimal interface, write a fake that implements it, inject at the call site.
// user_service.go
package user
type EmailSender interface {
Send(to, subject, body string) error
}
type UserRepository interface {
FindByID(id int) (*User, error)
Save(u *User) error
}
type UserService struct {
repo UserRepository
mailer EmailSender
}
func NewUserService(repo UserRepository, mailer EmailSender) *UserService {
return &UserService{repo: repo, mailer: mailer}
}
func (s *UserService) Register(name, email string) (*User, error) {
u := &User{Name: name, Email: email}
if err := s.repo.Save(u); err != nil {
return nil, err
}
_ = s.mailer.Send(email, "Welcome", "Thanks for signing up!")
return u, nil
}
// user_service_test.go
package user_test
type fakeMailer struct {
calls []struct{ to, subject, body string }
err error
}
func (m *fakeMailer) Send(to, subject, body string) error {
m.calls = append(m.calls, struct{ to, subject, body string }{to, subject, body})
return m.err
}
type fakeRepo struct {
saved []*User
err error
}
func (r *fakeRepo) FindByID(id int) (*User, error) {
for _, u := range r.saved {
if u.ID == id {
return u, nil
}
}
return nil, ErrNotFound
}
func (r *fakeRepo) Save(u *User) error {
if r.err != nil {
return r.err
}
u.ID = len(r.saved) + 1
r.saved = append(r.saved, u)
return nil
}
func TestUserService_Register(t *testing.T) {
tests := []struct {
name string
repoErr error
mailerErr error
wantEmail bool
wantErr bool
}{
{"success", nil, nil, true, false},
{"repo error", ErrDB, nil, false, true},
{"mailer error silenced", nil, ErrMail, true, false}, // mailer error ignored
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &fakeRepo{err: tt.repoErr}
mailer := &fakeMailer{err: tt.mailerErr}
svc := NewUserService(repo, mailer)
user, err := svc.Register("Alice", "alice@example.com")
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("name = %q; want %q", user.Name, "Alice")
}
if tt.wantEmail && len(mailer.calls) == 0 {
t.Error("expected email to be sent")
}
})
}
}
type spyLogger struct {
mu sync.Mutex
entries []string
}
func (l *spyLogger) Log(msg string) {
l.mu.Lock()
defer l.mu.Unlock()
l.entries = append(l.entries, msg)
}
func (l *spyLogger) Contains(s string) bool {
l.mu.Lock()
defer l.mu.Unlock()
for _, e := range l.entries {
if strings.Contains(e, s) {
return true
}
}
return false
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
func BenchmarkWithSetup(b *testing.B) {
data := generateLargeDataset() // Don't time this
b.ResetTimer() // Start timing from here
for i := 0; i < b.N; i++ {
Process(data)
}
}
func BenchmarkWithTeardown(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
data := prepareData() // Not timed
b.StartTimer()
Process(data)
}
}
func BenchmarkSort(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
data := make([]int, size)
for i := range data {
data[i] = rand.Intn(size)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
input := make([]int, len(data))
copy(input, data)
b.StartTimer()
sort.Ints(input)
}
})
}
}
func BenchmarkAllocations(b *testing.B) {
b.ReportAllocs() // Show allocs/op in output
for i := 0; i < b.N; i++ {
result := buildString("hello", "world")
_ = result
}
}
// Run with:
// go test -bench=BenchmarkAllocations -benchmem
// Output:
// BenchmarkAllocations-8 5000000 243 ns/op 48 B/op 2 allocs/op
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkSort
# Include memory stats
go test -bench=. -benchmem
# Run 5 seconds per benchmark
go test -bench=. -benchtime=5s
# Run 10 iterations minimum
go test -bench=. -benchtime=10x
# Compare with benchstat
go test -bench=. -count=5 > old.txt
# make changes
go test -bench=. -count=5 > new.txt
benchstat old.txt new.txt
// fuzz_test.go
package parser_test
import (
"testing"
"unicode/utf8"
)
func FuzzParseInput(f *testing.F) {
// Seed corpus — known-good inputs
f.Add("hello world")
f.Add("")
f.Add("special: !@#$%")
f.Add("unicode: 日本語")
f.Fuzz(func(t *testing.T, input string) {
// The fuzzer mutates `input` automatically
// Property: must not panic
result, err := ParseInput(input)
// Property: if no error, result must be valid UTF-8
if err == nil {
if !utf8.ValidString(result) {
t.Errorf("ParseInput(%q) returned non-UTF-8 output", input)
}
}
// Property: output length <= input length (example invariant)
if len(result) > len(input) {
t.Errorf("output longer than input: %d > %d", len(result), len(input))
}
})
}
# Run existing corpus only (fast)
go test -run FuzzParseInput
# Run fuzzer for 30 seconds
go test -fuzz FuzzParseInput -fuzztime=30s
# Fuzz until failure found
go test -fuzz FuzzParseInput
# Corpus is stored in testdata/fuzz/FuzzParseInput/
package api_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestGetUserHandler(t *testing.T) {
tests := []struct {
name string
userID string
wantStatus int
wantName string
}{
{"existing user", "1", http.StatusOK, "Alice"},
{"not found", "999", http.StatusNotFound, ""},
{"invalid id", "abc", http.StatusBadRequest, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/"+tt.userID, nil)
w := httptest.NewRecorder()
handler := NewUserHandler(fakeUserRepo)
handler.ServeHTTP(w, req)
if w.Code != tt.wantStatus {
t.Errorf("status = %d; want %d", w.Code, tt.wantStatus)
}
if tt.wantName != "" {
var user User
json.NewDecoder(w.Body).Decode(&user)
if user.Name != tt.wantName {
t.Errorf("name = %q; want %q", user.Name, tt.wantName)
}
}
})
}
}
func TestCreateUserHandler(t *testing.T) {
body := `{"name":"Alice","email":"alice@example.com"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler := NewUserHandler(fakeUserRepo)
handler.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Errorf("status = %d; want %d", w.Code, http.StatusCreated)
}
var created User
json.NewDecoder(w.Body).Decode(&created)
if created.ID == 0 {
t.Error("expected ID to be set")
}
}
func TestWithServer(t *testing.T) {
router := setupRouter()
server := httptest.NewServer(router)
defer server.Close()
resp, err := http.Get(server.URL + "/health")
if err != nil {
t.Fatalf("GET /health: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d; want %d", resp.StatusCode, http.StatusOK)
}
}
# Show coverage percentage
go test ./... -cover
# Generate coverage profile
go test ./... -coverprofile=coverage.out
# View in browser
go tool cover -html=coverage.out
# Show function-level coverage
go tool cover -func=coverage.out
# Fail if coverage < 80%
go test ./... -cover -coverprofile=coverage.out
go tool cover -func=coverage.out | grep total | awk '{if ($3 < 80) exit 1}'
// Package: use _test suffix for black-box tests
package mypackage_test // can only use exported symbols
// Or use same package for white-box tests
package mypackage // can access unexported symbols
// Naming: Test<FunctionName>_<Scenario>
func TestParseDate_ValidFormat(t *testing.T) {}
func TestParseDate_InvalidMonth(t *testing.T) {}
func TestParseDate_LeapYear(t *testing.T) {}
// Always call t.Helper() in helpers
func assertJSON(t *testing.T, body *bytes.Buffer, want interface{}) {
t.Helper() // Makes error point to caller, not here
var got interface{}
if err := json.NewDecoder(body).Decode(&got); err != nil {
t.Fatalf("decode JSON: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("JSON mismatch:\ngot %+v\nwant %+v", got, want)
}
}
// BAD: Shared mutable state
var globalDB *sql.DB // leaks between tests
// GOOD: Create per-test
func TestSomething(t *testing.T) {
db := setupTestDB(t) // created and cleaned up per test
...
}
// BAD: Testing implementation details
if user.internalCache != nil { ... }
// GOOD: Test observable behavior
if user.Name != "Alice" { ... }
// BAD: time.Sleep in tests
time.Sleep(100 * time.Millisecond)
// GOOD: Synchronize explicitly
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timed out")
}
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: true
- name: Run tests
run: go test ./... -race -coverprofile=coverage.out
- name: Check coverage
run: |
go tool cover -func=coverage.out | grep total
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
echo "Coverage: $COVERAGE%"
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: coverage.out
| Feature | Command/Usage |
|---|---|
| Run tests | go test ./... |
| Race detector | go test -race ./... |
| Specific test | go test -run TestName |
| Verbose | go test -v ./... |
| Benchmark | go test -bench=. |
| Benchmark + mem | go test -bench=. -benchmem |
| Fuzz | go test -fuzz FuzzName -fuzztime=30s |
| Coverage | go test -cover ./... |
| Coverage profile | go test -coverprofile=c.out && go tool cover -html=c.out |
| Parallel tests | t.Parallel() inside test |
| Capture variable | tt := tt before t.Run (Go <1.22) |
| Skip long tests | testing.Short() check + -short flag |