Giao diện
🧪 Testing & Benchmarks
"Untested code is broken code."
Go's testing toolchain là một trong những điểm mạnh nhất của ngôn ngữ.
Go's testing toolchain là một trong những điểm mạnh nhất của ngôn ngữ.
📋 Testing Fundamentals
Test File Convention
mypackage/
├── user.go # Implementation
├── user_test.go # Unit tests
├── user_bench_test.go # Benchmarks (optional separate file)
└── testdata/ # Test fixtures
└── users.jsonBasic Test Structure
go
// user_test.go
package user
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
// Parallel test
func TestAddParallel(t *testing.T) {
t.Parallel() // Mark as parallel-safe
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}📊 Table-Driven Tests (The Go Way)
Standard Pattern
go
func TestCalculate(t *testing.T) {
tests := []struct {
name string
input Input
expected Output
wantErr bool
}{
{
name: "positive numbers",
input: Input{A: 5, B: 3},
expected: Output{Sum: 8, Product: 15},
wantErr: false,
},
{
name: "zero values",
input: Input{A: 0, B: 0},
expected: Output{Sum: 0, Product: 0},
wantErr: false,
},
{
name: "overflow",
input: Input{A: math.MaxInt64, B: 1},
expected: Output{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Calculate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.expected {
t.Errorf("got %v, want %v", got, tt.expected)
}
})
}
}Parallel Table Tests
go
func TestUserService_Create(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
}{
{"valid user", &User{Name: "Alice", Email: "alice@example.com"}, false},
{"empty name", &User{Name: "", Email: "bob@example.com"}, true},
{"invalid email", &User{Name: "Charlie", Email: "invalid"}, true},
}
for _, tt := range tests {
tt := tt // Capture range variable (pre-Go 1.22)
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Run subtests in parallel
svc := NewUserService(NewMockRepo())
err := svc.Create(context.Background(), tt.user)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}🎭 Mocking Strategies
⚔️ Tradeoff: Mocking Approaches
| Approach | Pros | Cons | When to Use |
|---|---|---|---|
| Hand-rolled | Simple, explicit | Manual effort | Small interfaces |
| gomock | Generated, type-safe | Learning curve | Large interfaces |
| mockery | Auto-generates | Dependency | testify users |
| testify/mock | Flexible assertions | Runtime type checking | Quick prototyping |
Hand-Rolled Mocks (Recommended)
go
// Interface in service package
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
}
// Mock in test file
type mockUserRepo struct {
getUserFn func(ctx context.Context, id int64) (*User, error)
createFn func(ctx context.Context, user *User) error
}
func (m *mockUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
return m.getUserFn(ctx, id)
}
func (m *mockUserRepo) Create(ctx context.Context, user *User) error {
return m.createFn(ctx, user)
}
// Usage in test
func TestUserService_GetUser(t *testing.T) {
mock := &mockUserRepo{
getUserFn: func(ctx context.Context, id int64) (*User, error) {
if id == 1 {
return &User{ID: 1, Name: "Alice"}, nil
}
return nil, ErrNotFound
},
}
svc := NewUserService(mock)
user, err := svc.GetUser(context.Background(), 1)
if err != nil {
t.Fatal(err)
}
if user.Name != "Alice" {
t.Errorf("got %s, want Alice", user.Name)
}
}gomock Generated Mocks
go
//go:generate mockgen -destination=mock_repo_test.go -package=service . UserRepository
func TestWithGoMock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
// Set expectations
repo.EXPECT().
GetByID(gomock.Any(), int64(1)).
Return(&User{ID: 1, Name: "Alice"}, nil)
svc := NewUserService(repo)
user, _ := svc.GetUser(context.Background(), 1)
if user.Name != "Alice" {
t.Error("unexpected name")
}
}🐳 Integration Testing with Testcontainers
go
// user_integration_test.go
//go:build integration
package repository_test
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestUserRepository_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
// Start PostgreSQL container
pgContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:15-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForListeningPort("5432/tcp"),
},
Started: true,
})
if err != nil {
t.Fatal(err)
}
defer pgContainer.Terminate(ctx)
// Get connection string
host, _ := pgContainer.Host(ctx)
port, _ := pgContainer.MappedPort(ctx, "5432")
dsn := fmt.Sprintf("postgres://test:test@%s:%s/testdb?sslmode=disable", host, port.Port())
// Connect and run migrations
db, err := pgx.Connect(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close(ctx)
// Run your tests
repo := NewUserRepository(db)
t.Run("create and get user", func(t *testing.T) {
user := &User{Name: "Test", Email: "test@example.com"}
if err := repo.Create(ctx, user); err != nil {
t.Fatal(err)
}
got, err := repo.GetByID(ctx, user.ID)
if err != nil {
t.Fatal(err)
}
if got.Name != user.Name {
t.Errorf("got %s, want %s", got.Name, user.Name)
}
})
}
)}📁 Golden File Testing
go
func TestMarshalUser_Golden(t *testing.T) {
user := &User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
}
got, err := json.MarshalIndent(user, "", " ")
if err != nil {
t.Fatal(err)
}
goldenFile := "testdata/user.golden"
if *update { // -update flag
os.WriteFile(goldenFile, got, 0644)
return
}
want, err := os.ReadFile(goldenFile)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Errorf("mismatch:\ngot:\n%s\nwant:\n%s", got, want)
}
}
var update = flag.Bool("update", false, "update golden files")🎲 Fuzzing (Go 1.18+)
go
func FuzzParseJSON(f *testing.F) {
// Seed corpus
f.Add([]byte(`{"name": "Alice"}`))
f.Add([]byte(`{"name": ""}`))
f.Add([]byte(`{}`))
f.Fuzz(func(t *testing.T, data []byte) {
var user User
err := json.Unmarshal(data, &user)
if err != nil {
return // Invalid JSON is expected
}
// Re-marshal should not panic
_, err = json.Marshal(&user)
if err != nil {
t.Errorf("marshal failed after successful unmarshal: %v", err)
}
})
}
// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s⚡ Benchmarking
Basic Benchmark
go
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "Hello, " + "World!"
}
}
// Run: go test -bench=. -benchmem
// Output:
// BenchmarkStringConcat-8 1000000000 0.25 ns/op 0 B/op 0 allocs/opComparing Implementations
go
func BenchmarkConcat_Plus(b *testing.B) {
strs := []string{"hello", "world", "foo", "bar"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := ""
for _, s := range strs {
result += s
}
_ = result
}
}
func BenchmarkConcat_Builder(b *testing.B) {
strs := []string{"hello", "world", "foo", "bar"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for _, s := range strs {
sb.WriteString(s)
}
_ = sb.String()
}
}
func BenchmarkConcat_Join(b *testing.B) {
strs := []string{"hello", "world", "foo", "bar"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = strings.Join(strs, "")
}
}
/*
Results:
BenchmarkConcat_Plus-8 5000000 380 ns/op 80 B/op 4 allocs/op
BenchmarkConcat_Builder-8 20000000 62 ns/op 64 B/op 1 allocs/op
BenchmarkConcat_Join-8 30000000 55 ns/op 32 B/op 1 allocs/op
*/Sub-benchmarks with Parameters
go
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000, 100000}
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()
clone := make([]int, len(data))
copy(clone, data)
b.StartTimer()
sort.Ints(clone)
}
})
}
}💻 Engineering Example: CI-Ready Test Suite
Test Helper Package
go
// internal/testutil/testutil.go
package testutil
import (
"context"
"testing"
"time"
)
// TestContext returns a context with deadline for tests
func TestContext(t *testing.T) context.Context {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
t.Cleanup(cancel)
return ctx
}
// AssertError checks for expected error
func AssertError(t *testing.T, got, want error) {
t.Helper()
if !errors.Is(got, want) {
t.Errorf("error = %v, want %v", got, want)
}
}
// AssertEqual uses reflect.DeepEqual
func AssertEqual[T any](t *testing.T, got, want T) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}Makefile Test Targets
makefile
.PHONY: test test-unit test-integration test-coverage test-race
# All tests
test: test-unit test-integration
# Unit tests only
test-unit:
go test -v -short ./...
# Integration tests (requires Docker)
test-integration:
go test -v -tags=integration ./...
# Coverage report
test-coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: coverage.html"
# Race detector
test-race:
go test -race ./...
# Benchmarks
bench:
go test -bench=. -benchmem ./...
# Fuzz (30 seconds)
fuzz:
go test -fuzz=. -fuzztime=30s ./...✅ Ship-to-Prod Checklist
Test Quality
- [ ] Coverage > 80% cho business logic
- [ ] Table-driven tests cho edge cases
- [ ] Parallel tests where safe (
t.Parallel()) - [ ] Integration tests cho data layer
Test Infrastructure
- [ ] CI runs
go test -race ./... - [ ] Integration tests với testcontainers
- [ ] Coverage reported và tracked
- [ ] Golden files cho snapshot testing
Benchmarks
- [ ] Benchmark critical paths
- [ ] Track
-benchmemallocations - [ ] Compare before/after changes
- [ ] Run in CI for regression detection
Anti-patterns to Avoid
- [ ] No
time.Sleep()in tests (use channels/waitgroups) - [ ] No hardcoded ports (use dynamic allocation)
- [ ] No dependency on test order
- [ ] No shared mutable state between tests
📊 Summary
| Topic | Command/Pattern |
|---|---|
| Run tests | go test ./... |
| Verbose | go test -v ./... |
| Race detector | go test -race ./... |
| Coverage | go test -coverprofile=c.out ./... |
| Benchmarks | go test -bench=. -benchmem ./... |
| Fuzzing | go test -fuzz=Fuzz -fuzztime=30s |
| Short mode | go test -short ./... |
➡️ Tiếp theo
Testing nắm vững rồi! Tiếp theo: HTTP Services - Building production HTTP APIs.