协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
testing-strategies【开发与工程】Implements comprehensive testing strategies for Go including unit tests, integration tests, benchmarks, table-driven tests, and mock patterns.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: testing-strategies description: Implements comprehensive testing strategies for Go including unit tests, integration tests, benchmarks, table-driven tests, and mock patterns. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: go role: implementation scope: implementation output-format: code content-types:
Senior QA engineer implementing comprehensive testing strategies for Go applications. This skill covers table-driven tests, mocking, benchmarks, integration testing, and fuzzing following Go's native testing conventions.
t.Parallel() for independent tests that don't share statego test -race on all tests to detect data racesUse this skill when:
Avoid this skill for:
Identify Testable Units — Determine what functions and methods need tests. Checkpoint: Every exported function has at least one test case.
Write Table-Driven Tests — Use tables for functions with multiple input/output scenarios. Checkpoint: Each table entry has a name, inputs, expected output, and optional error expectation.
Create Mock Implementations — Define interfaces and create mock implementations for testing. Checkpoint: Mocks implement the same interface as the real dependency.
Write Integration Tests — Test with real databases or services using test containers.
Checkpoint: Integration tests are marked with //go:build integration tag.
Add Benchmarks — Write benchmarks for performance-critical code paths. Checkpoint: Benchmarks run long enough to get stable results (>1s wall time).
Table-driven tests are the Go convention for testing functions with multiple cases.
// ❌ BAD: separate test functions for each case — code duplication, hard to add cases
func TestAdd_Positive(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
func TestAdd_Zero(t *testing.T) {
result := Add(0, 0)
if result != 0 {
t.Errorf("Add(0, 0) = %d; want 0", result)
}
}
func TestAdd_Negative(t *testing.T) {
result := Add(-1, -2)
if result != -3 {
t.Errorf("Add(-1, -2) = %d; want -3", result)
}
}
func TestAdd_Mixed(t *testing.T) {
result := Add(-1, 5)
if result != 4 {
t.Errorf("Add(-1, 5) = %d; want 4", result)
}
}
What's wrong:
result := Add(a, b) and if result != want// TestAdd tests the Add function with various input combinations.
func TestAdd(t *testing.T) {
t.Parallel()
tests := []struct {
name string
a, b int
want int
}{
{"positive + positive", 2, 3, 5},
{"zero + zero", 0, 0, 0},
{"negative + negative", -1, -2, -3},
{"mixed signs", -1, 5, 4},
{"large positive", 1000000, 2000000, 3000000},
{"overflow edge case", math.MaxInt32 - 1, 1, math.MaxInt32},
{"negative overflow edge case", math.MinInt32 + 1, -1, math.MinInt32},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
Why this works:
t.Run(tt.name, ...) creates sub-tests — each case appears separately in test outputt.Parallel() on sub-tests runs cases concurrently — faster test executionMock interfaces, never concrete types. Go's interface-based design makes this natural.
// ❌ BAD: concrete type dependency — cannot be mocked, tight coupling
package service
import "myapp/internal/db"
type UserService struct {
dbClient *db.Client // concrete type — impossible to mock
}
// Test without mocking — requires a real database connection
func TestCreateUser(t *testing.T) {
// Must set up a real database — slow, fragile, not isolated
dsn := "postgres://localhost/testdb"
dbClient, err := db.NewClient(dsn)
if err != nil {
t.Fatalf("failed to connect to test database: %v", err)
}
defer dbClient.Close()
// Flush test data
dbClient.Exec("DELETE FROM users")
svc := &UserService{dbClient: dbClient}
user, err := svc.CreateUser(context.Background(), "test")
if err != nil {
t.Fatalf("CreateUser failed: %v", err)
}
if user.Name != "test" {
t.Errorf("user name = %q; want %q", user.Name, "test")
}
}
What's wrong:
*db.Client is a concrete type — cannot substitute a mock// package: internal/user
// UserRepository is the interface for user persistence.
// Defined in the domain package; implemented by infrastructure.
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
}
// Service depends on the UserRepository interface.
type Service struct {
repo UserRepository
}
// CreateUser validates and creates a new user.
func (s *Service) CreateUser(ctx context.Context, name string) (*User, error) {
if name == "" {
return nil, fmt.Errorf("create user: name must not be empty")
}
user := &User{
Name: strings.TrimSpace(name),
ID: generateID(),
State: UserStateActive,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
// package: internal/user
// mockUserRepository is a test double for UserRepository.
// Embedded UserRepository ensures it implements the interface.
type mockUserRepository struct {
UserRepository // embedded — satisfies interface
createFn func(ctx context.Context, user *User) error
getFn func(ctx context.Context, id string) (*User, error)
}
// newMockUserRepository creates a mock repository with optional callbacks.
func newMockUserRepository(createFn func(ctx context.Context, user *User) error,
getFn func(ctx context.Context, id string) (*User, error),
) *mockUserRepository {
return &mockUserRepository{
createFn: createFn,
getFn: getFn,
}
}
// Create implements UserRepository.Create with the provided callback.
func (m *mockUserRepository) Create(ctx context.Context, user *User) error {
if m.createFn != nil {
return m.createFn(ctx, user)
}
return nil
}
// GetByID implements UserRepository.GetByID with the provided callback.
func (m *mockUserRepository) GetByID(ctx context.Context, id string) (*User, error) {
if m.getFn != nil {
return m.getFn(ctx, id)
}
return nil, sql.ErrNoRows
}
// package: internal/user
// TestUserService_CreateUser tests user creation with a mock repository.
func TestUserService_CreateUser(t *testing.T) {
t.Parallel()
tests := []struct {
name string
inputName string
createErr error
wantErr bool
wantName string
}{
{
name: "valid name",
inputName: "Alice",
wantName: "Alice",
wantErr: false,
},
{
name: "empty name",
inputName: "",
wantErr: true,
},
{
name: "whitespace name",
inputName: " ",
wantName: "",
wantErr: true,
},
{
name: "database error",
inputName: "Bob",
createErr: fmt.Errorf("connection refused"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mock := newMockUserRepository(
func(ctx context.Context, user *User) error { return tt.createErr },
nil, // getFn not needed for Create test
)
svc := NewService(mock)
user, err := svc.CreateUser(context.Background(), tt.inputName)
if tt.wantErr {
if err == nil {
t.Error("CreateUser returned nil error; want error")
}
return
}
if err != nil {
t.Fatalf("CreateUser returned unexpected error: %v", err)
}
if user.Name != tt.wantName {
t.Errorf("user.Name = %q; want %q", user.Name, tt.wantName)
}
})
}
}
Why this works:
UserRepository interface is mocked — tests run without a real databaseembedded UserRepository ensures the mock implements the interfacet.Run with table-driven tests covers all cases from one functionBenchmarks measure performance; integration tests verify correct behavior with real dependencies.
// ❌ BAD: no benchmarks, no integration tests — performance regressions go undetected
package search
func Search(query string, documents []Document) []Result {
var results []Result
for _, doc := range documents {
if contains(doc.Content, query) {
results = append(results, Result{Doc: doc, Score: score(doc, query)})
}
}
return results
}
// No benchmark — performance is not measured
// No integration test — correctness with real data is not verified
What's wrong:
contains() is a simple string search — O(n*m) per document, very slow at scalescore())// package: search
// BenchmarkSearch measures search performance with varying document counts.
func BenchmarkSearch(b *testing.B) {
// Generate test data once (outside the loop)
documents := make([]Document, 10000)
for i := range documents {
documents[i] = Document{
ID: fmt.Sprintf("doc-%d", i),
Title: fmt.Sprintf("Document %d", i),
Content: generateContent(i),
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Search("Document", documents)
}
}
// BenchmarkSearchIndex demonstrates the performance benefit of indexing.
func BenchmarkSearchIndex(b *testing.B) {
documents := make([]Document, 10000)
for i := range documents {
documents[i] = Document{
ID: fmt.Sprintf("doc-%d", i),
Title: fmt.Sprintf("Document %d", i),
Content: generateContent(i),
}
}
// Build index once (outside the loop)
idx := NewIndex()
for _, doc := range documents {
idx.Add(doc)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
idx.Search("Document")
}
}
// package: search
//go:build integration
// TestSearchIntegration tests search against real data in a real Elasticsearch instance.
// Run with: go test -tags=integration ./search
func TestSearchIntegration(t *testing.T) {
t.Skip("skip integration test unless -tags=integration is set")
ctx := context.Background()
es, err := NewClient("http://localhost:9200")
if err != nil {
t.Fatalf("failed to create Elasticsearch client: %v", err)
}
defer es.Close()
// Setup: index test documents
testDocs := []Document{
{ID: "1", Title: "Go concurrency", Content: "Goroutines and channels"},
{ID: "2", Title: "Go testing", Content: "Table-driven tests and benchmarks"},
{ID: "3", Title: "Python asyncio", Content: "Async and await patterns"},
}
for _, doc := range testDocs {
if err := es.Index(ctx, "docs", doc); err != nil {
t.Fatalf("failed to index document: %v", err)
}
}
// Refresh index to make documents searchable
if err := es.Refresh(ctx, "docs"); err != nil {
t.Fatalf("failed to refresh index: %v", err)
}
// Test: search for "Go"
results, err := es.Search(ctx, "docs", "Go")
if err != nil {
t.Fatalf("search failed: %v", err)
}
if len(results) != 2 {
t.Errorf("found %d results; want 2", len(results))
}
// Verify each result contains "Go" in title or content
for _, r := range results {
doc := r.Document.(Document)
if !contains(doc.Title, "Go") && !contains(doc.Content, "Go") {
t.Errorf("result document %q does not contain 'Go'", doc.ID)
}
}
}
// generateContent creates realistic test content for benchmarking.
func generateContent(seed int) string {
words := []string{"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog"}
var content strings.Builder
for i := 0; i < 100; i++ {
word := words[seed+i%len(words)]
if i > 0 {
content.WriteString(" ")
}
content.WriteString(word)
}
return content.String()
}
Why this works:
BenchmarkSearch measures baseline linear search performanceBenchmarkSearchIndex measures indexed search — the comparison shows the speedupb.ResetTimer() ensures warm-up time (index building) doesn't skew results//go:build integration tag excludes integration tests from normal test runs"valid email", "empty string", "SQL injection")t.Run(tt.name, ...) to create sub-tests for each table entrygo test -race to verify no data races in all tests//go:build integration)t.Fatal in sub-tests — use t.Error instead (allows other sub-tests to run)t.Skip() in CI — use build tags or environment checks insteadtime.Sleep in tests — use channels, context cancellation, or test helpers| Skill | Purpose |
|---|---|
best-practices | Go idioms, naming, and code organization for testable code |
modular-design | Interface-based design that enables clean mocking |
database-patterns | Repository patterns that work with mock implementations |