go-testing-advanced
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.
Best use case
go-testing-advanced is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using go-testing-advanced should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/go-testing-advanced/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How go-testing-advanced Compares
| Feature / Agent | go-testing-advanced | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Go Testing — Advanced Patterns
> This skill extends [go-testing](../go-testing/SKILL.md) with mocking, benchmarks, fuzzing, HTTP testing, and CI/CD.
## When to Activate
- Writing interface-based mocks without external libraries
- Benchmarking performance-critical code
- Using Go 1.18+ fuzzing for input validation
- Testing HTTP handlers with `net/http/httptest`
- Setting up CI/CD test pipelines
- Comparing benchmark results across commits using `benchstat` to detect performance regressions
- Adding a fuzz corpus and property-based assertions to a parser or input validation function
- Configuring a GitHub Actions workflow that enforces Go test coverage thresholds on every pull request
## Interface-Based Mocking
The idiomatic Go approach: define a minimal interface, write a fake that implements it, inject at the call site.
### Defining the Interface
```go
// 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
}
```
### Writing the Fake
```go
// 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")
}
})
}
}
```
### Spy Pattern (Record Calls)
```go
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
}
```
## Benchmarks
### Basic Benchmark
```go
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
```
### Reset and Stop Timers
```go
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)
}
}
```
### Size-Parametrized Benchmark
```go
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)
}
})
}
}
```
### Allocation Benchmark
```go
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
```
### Running Benchmarks
```bash
# 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
```
## Fuzzing (Go 1.18+)
```go
// 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))
}
})
}
```
```bash
# 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/
```
## HTTP Handler Testing with httptest
### Testing Individual Handlers
```go
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")
}
}
```
### Testing with a Real Server
```go
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)
}
}
```
## Test Coverage
```bash
# 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}'
```
## Best Practices
### Test File Conventions
```go
// 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) {}
```
### Test Helper Best Practices
```go
// 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)
}
}
```
### Avoid Test Anti-Patterns
```go
// 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")
}
```
## CI/CD Integration
### GitHub Actions
```yaml
# .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
```
## Quick Reference
| 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 |Related Skills
visual-testing
Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.
typescript-testing
TypeScript testing patterns: Vitest for unit/integration, Playwright for E2E, MSW for API mocking, Testing Library for React components. Core TDD methodology for TypeScript/JavaScript projects.
typescript-patterns-advanced
Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.
tdd-workflow-advanced
TDD anti-patterns — writing code before tests, testing implementation details instead of behavior, using waitForTimeout as a sync strategy, chaining tests that share state, mocking the system under test instead of its dependencies.
swift-testing
Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.
swift-protocol-di-testing
Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.
swift-patterns-advanced
Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.
serverless-patterns-advanced
Advanced Serverless patterns — Lambda idempotency (Lambda Powertools + DynamoDB persistence layer), Lambda cost model (pricing formula, break-even vs containers), and CloudWatch Insights observability queries for cold starts, duration, and errors.
security-review-advanced
Security anti-patterns — localStorage token storage (XSS risk), trusting client-side authorization checks, reflecting full error details to clients, blacklist vs whitelist input validation, using npm install instead of npm ci in CI pipelines.
scala-testing
Scala testing with ScalaTest, MUnit, and ScalaCheck: FunSpec/FlatSpec test structure, property-based testing with forAll, mocking with MockitoSugar, Cats Effect testing with munit-cats-effect (runTest/IOSuite), ZIO Test, Testcontainers-Scala for database integration tests, and CI integration with sbt. Use when writing or reviewing Scala tests.
rust-testing
Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.
rust-testing-advanced
Advanced Rust testing anti-patterns and corrections — cfg(test) placement, expect() over unwrap(), mockall expectation ordering, executor mixing (#[tokio::test] vs block_on), PgPool isolation with