Go语言测试与Benchmark:测试驱动开发的实践指南

引言

测试是保证软件质量的重要手段。Go语言在设计之初就将测试作为标准库的一部分,提供了简洁而强大的测试框架testing。本文将全面介绍Go语言测试的各个方面,从单元测试到基准测试,从测试夹具到Mock技术,帮助读者掌握Go测试的最佳实践。

一、单元测试编写规范

1.1 测试文件命名与结构

Go的测试文件必须以_test.go结尾:

复制代码
// math.go
package math
​
func Add(a, b int) int {
    return a + b
}
​
func Subtract(a, b int) int {
    return a - b
}
​
// math_test.go
package math
​
import (
    "testing"
)
​
// 测试函数必须以Test开头,参数为*testing.T
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
​
    if result != expected {
        t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
    }
}
​
func TestSubtract(t *testing.T) {
    result := Subtract(5, 3)
    expected := 2
​
    if result != expected {
        t.Errorf("Subtract(5, 3) = %d; expected %d", result, expected)
    }
}

1.2 运行测试

复制代码
# 运行所有测试
go test ./...
​
# 运行指定测试
go test -v ./... -run TestAdd
​
# 运行匹配模式的测试
go test -v ./... -run "TestAdd|TestSubtract"
​
# 显示测试覆盖率
go test -v -cover ./...
​
# 生成覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

1.3 断言辅助函数

Go标准库没有内置断言,常用自定义断言:

复制代码
package assert
​
import (
    "reflect"
    "testing"
)
​
func Equal(t *testing.T, expected, actual interface{}) {
    if !reflect.DeepEqual(expected, actual) {
        t.Errorf("Expected: %v, Got: %v", expected, actual)
    }
}
​
func Nil(t *testing.T, obj interface{}) {
    if obj != nil {
        t.Errorf("Expected nil, Got: %v", obj)
    }
}
​
func NotNil(t *testing.T, obj interface{}) {
    if obj == nil {
        t.Errorf("Expected non-nil value")
    }
}
​
func True(t *testing.T, cond bool, msg string) {
    if !cond {
        t.Errorf("Expected true, %s", msg)
    }
}
​
func False(t *testing.T, cond bool, msg string) {
    if cond {
        t.Errorf("Expected false, %s", msg)
    }
}

1.4 完整示例

复制代码
// stringutil.go
package stringutil
​
import (
    "strings"
)
​
func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}
​
func ToUpper(s string) string {
    return strings.ToUpper(s)
}
​
func Contains(s, substr string) bool {
    return strings.Contains(s, substr)
}
​
// stringutil_test.go
package stringutil
​
import (
    "testing"
)
​
func TestReverse(t *testing.T) {
    testCases := []struct {
        input    string
        expected string
    }{
        {"hello", "olleh"},
        {"world", "dlrow"},
        {"", ""},
        {"a", "a"},
        {"ab", "ba"},
        {"中文", "文中"},
    }
​
    for _, tc := range testCases {
        result := Reverse(tc.input)
        if result != tc.expected {
            t.Errorf("Reverse(%q) = %q; expected %q", tc.input, result, tc.expected)
        }
    }
}
​
func TestToUpper(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello", "HELLO"},
        {"Hello", "HELLO"},
        {"HELLO", "HELLO"},
        {"", ""},
    }
​
    for _, tt := range tests {
        result := ToUpper(tt.input)
        if result != tt.expected {
            t.Errorf("ToUpper(%q) = %q; expected %q", tt.input, result, tt.expected)
        }
    }
}
​
func TestContains(t *testing.T) {
    if !Contains("hello world", "world") {
        t.Error("Contains should return true when substring exists")
    }
​
    if Contains("hello", "world") {
        t.Error("Contains should return false when substring doesn't exist")
    }
}

二、测试夹具(Setup/Teardown)

2.1 包的Setup和Teardown

使用TestMain函数实现:

复制代码
package math
​
import (
    "os"
    "testing"
)
​
var (
    testDB   *DB
    testData []int
)
​
func TestMain(m *testing.M) {
    // Setup - 运行所有测试前执行
    testDB = setupTestDB()
    testData = []int{1, 2, 3, 4, 5}
​
    // 运行所有测试
    exitCode := m.Run()
​
    // Teardown - 所有测试后执行
    teardownTestDB(testDB)
​
    os.Exit(exitCode)
}
​
func setupTestDB() *DB {
    // 创建测试数据库
    return NewDB("test_connection_string")
}
​
func teardownTestDB(db *DB) {
    // 清理测试数据库
    db.Close()
}
​
func TestSum(t *testing.T) {
    result := Sum(testData...)
    expected := 15
    if result != expected {
        t.Errorf("Sum(%v) = %d; expected %d", testData, result, expected)
    }
}

2.2 每个测试的Setup/Teardown

复制代码
package repository
​
import (
    "testing"
)
​
func TestUserRepository(t *testing.T) {
    // 准备测试数据
    repo := setupRepo(t)
    defer repo.Close()
    seedTestData(t, repo)
​
    t.Run("TestGetUser", func(t *testing.T) {
        user, err := repo.GetUser(1)
        if err != nil {
            t.Fatalf("GetUser failed: %v", err)
        }
        if user.Name != "Alice" {
            t.Errorf("Expected name Alice, got %s", user.Name)
        }
    })
​
    t.Run("TestUpdateUser", func(t *testing.T) {
        err := repo.UpdateUser(1, "Bob")
        if err != nil {
            t.Fatalf("UpdateUser failed: %v", err)
        }
​
        user, _ := repo.GetUser(1)
        if user.Name != "Bob" {
            t.Errorf("Expected name Bob, got %s", user.Name)
        }
    })
}
​
func setupRepo(t *testing.T) *Repository {
    t.Helper()
    return NewRepository("test_connection")
}
​
func seedTestData(t *testing.T, repo *Repository) {
    t.Helper()
    repo.Clear()
    repo.CreateUser(&User{ID: 1, Name: "Alice"})
    repo.CreateUser(&User{ID: 2, Name: "Bob"})
}

2.3 Table-Driven测试

复制代码
package parser
​
import (
    "testing"
)
​
func TestParseInt(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        base    int
        expected int64
        hasError bool
    }{
        {"decimal", "123", 10, 123, false},
        {"hex", "0xFF", 16, 255, false},
        {"binary", "1010", 2, 10, false},
        {"invalid", "xyz", 10, 0, true},
        {"negative", "-42", 10, -42, false},
        {"empty", "", 10, 0, true},
    }
​
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := ParseInt(tt.input, tt.base)
​
            if tt.hasError {
                if err == nil {
                    t.Errorf("Expected error for input %q", tt.input)
                }
                return
            }
​
            if err != nil {
                t.Errorf("Unexpected error: %v", err)
                return
            }
​
            if result != tt.expected {
                t.Errorf("ParseInt(%q, %d) = %d; expected %d",
                    tt.input, tt.base, result, tt.expected)
            }
        })
    }
}

三、子测试与子基准测试

3.1 子测试组织

复制代码
package example
​
import (
    "testing"
)
​
func TestMathOperations(t *testing.T) {
    t.Run("Addition", func(t *testing.T) {
        if Add(2, 3) != 5 {
            t.Error("Add failed")
        }
    })
​
    t.Run("Subtraction", func(t *testing.T) {
        if Subtract(5, 3) != 2 {
            t.Error("Subtract failed")
        }
    })
​
    t.Run("Multiplication", func(t *testing.T) {
        t.Run("Positive", func(t *testing.T) {
            if Multiply(2, 3) != 6 {
                t.Error("Multiply failed for positive numbers")
            }
        })
​
        t.Run("Negative", func(t *testing.T) {
            if Multiply(-2, 3) != -6 {
                t.Error("Multiply failed for negative numbers")
            }
        })
    })
}
​
// 并行子测试
func TestDatabaseQueries(t *testing.T) {
    queries := []string{"SELECT 1", "SELECT 2", "SELECT 3"}
​
    for _, query := range queries {
        t.Run(query, func(t *testing.T) {
            t.Parallel() // 标记为可并行
            // 执行查询测试
        })
    }
}

3.2 跳过测试

复制代码
func TestSlowOperation(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping slow test in short mode")
    }
    // 执行慢速测试
}
​
func TestOSFeatures(t *testing.T) {
    if runtime.GOOS == "windows" {
        t.Skip("Skipping test on Windows")
    }
    // 执行Linux特性测试
}
​
func TestRequiresAuth(t *testing.T) {
    if os.Getenv("TEST_AUTH") == "" {
        t.Skip("TEST_AUTH environment variable not set")
    }
    // 执行需要认证的测试
}

四、Benchmark基准测试编写

4.1 基本基准测试

复制代码
package benchmark
​
import (
    "testing"
)
​
func BenchmarkAdd(b *testing.B) {
    var result int
    for i := 0; i < b.N; i++ {
        result = Add(i, i+1)
    }
    _ = result // 防止编译器优化
}
​
func BenchmarkStringConcat(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        s := "hello" + " " + "world"
        _ = s
    }
}
​
func BenchmarkStringBuilder(b *testing.B) {
    var sb strings.Builder
    for i := 0; i < b.N; i++ {
        sb.Reset()
        sb.WriteString("hello")
        sb.WriteString(" ")
        sb.WriteString("world")
        _ = sb.String()
    }
}

4.2 运行基准测试

复制代码
# 运行基准测试
go test -bench=. ./...
​
# 运行指定基准测试
go test -bench=BenchmarkAdd ./...
​
# 显示内存分配统计
go test -bench=. -benchmem ./...
​
# 运行特定时间的基准测试
go test -bench=. -benchtime=5s ./...
​
# 统计CPU缓存命中率
go test -bench=. -benchprofile=cpu.prof ./...

4.3 对比基准测试

复制代码
package benchmark
​
import (
    "strings"
    "testing"
)
​
func BenchmarkConcat(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        result := ""
        for j := 0; j < 100; j++ {
            result += "a"
        }
        _ = result
    }
}
​
func BenchmarkBuilder(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}
​
func BenchmarkGrow(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sb := strings.NewBuilder()
        sb.Grow(100) // 预分配容量
        for j := 0; j < 100; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}

4.4 基准测试结果解析

运行go test -bench=. -benchmem的结果示例:

复制代码
goos: linux
goarch: amd64
BenchmarkConcat-8      1000000    1123 ns/op    896 B/op    99 allocs/op
BenchmarkBuilder-8     5000000     312 ns/op     128 B/op     1 allocs/op
BenchmarkGrow-8        8000000     189 ns/op     112 B/op     1 allocs/op
  • 1000000:测试运行的次数

  • 1123 ns/op:每次操作耗时

  • 896 B/op:每次操作内存分配

  • 99 allocs/op:每次操作分配次数

五、测试覆盖率分析

5.1 查看覆盖率

复制代码
# 生成覆盖率报告
go test -coverprofile=coverage.out ./...
​
# 查看文本覆盖率统计
go tool cover -func=coverage.out
​
# 生成HTML覆盖率报告
go tool cover -html=coverage.out -o coverage.html

5.2 覆盖率模式

复制代码
# 按包统计
go test -coverprofile=coverage.out ./...
​
# 按函数统计
go tool cover -func=coverage.out
​
# 设置覆盖率阈值(CI中使用)
go test -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -func=coverage.out | grep "total:" | awk '{print $3}' | sed 's/%//'

5.3 高覆盖率测试策略

复制代码
package calculator
​
import (
    "errors"
    "testing"
)
​
func TestDivide(t *testing.T) {
    tests := []struct {
        name      string
        dividend  float64
        divisor   float64
        expected  float64
        expectErr error
    }{
        {"normal", 10, 2, 5, nil},
        {"with zero", 10, 0, 0, ErrDivisionByZero},
        {"negative", -10, 2, -5, nil},
        {"decimal", 10.5, 2, 5.25, nil},
    }
​
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Divide(tt.dividend, tt.divisor)
​
            if tt.expectErr != nil {
                if !errors.Is(err, tt.expectErr) {
                    t.Errorf("Expected error %v, got %v", tt.expectErr, err)
                }
                return
            }
​
            if err != nil {
                t.Errorf("Unexpected error: %v", err)
                return
            }
​
            if result != tt.expected {
                t.Errorf("Divide(%f, %f) = %f; expected %f",
                    tt.dividend, tt.divisor, result, tt.expected)
            }
        })
    }
}
​
// 边界条件测试
func TestDivideEdgeCases(t *testing.T) {
    // 极大数
    _, err := Divide(1e308, 0.1)
    if err == nil {
        t.Error("Expected overflow error for very large numbers")
    }
​
    // 极小数
    result, _ := Divide(0.0000001, 1000000)
    if result == 0 {
        t.Error("Precision loss too high")
    }
}

六、Mock技术与接口测试

6.1 接口Mock

复制代码
package repository
​
// 定义接口
type UserRepository interface {
    GetUser(id int) (*User, error)
    CreateUser(user *User) error
    UpdateUser(user *User) error
    DeleteUser(id int) error
    ListUsers() ([]User, error)
}
​
// Mock实现
type MockUserRepository struct {
    users    map[int]*User
    nextID   int
    err      error
}
​
func NewMockUserRepository() *MockUserRepository {
    return &MockUserRepository{
        users:  make(map[int]*User),
        nextID: 1,
    }
}
​
func (m *MockUserRepository) GetUser(id int) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    user, ok := m.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return user, nil
}
​
func (m *MockUserRepository) CreateUser(user *User) error {
    if m.err != nil {
        return m.err
    }
    user.ID = m.nextID
    m.nextID++
    m.users[user.ID] = user
    return nil
}
​
func (m *MockUserRepository) UpdateUser(user *User) error {
    if m.err != nil {
        return m.err
    }
    if _, ok := m.users[user.ID]; !ok {
        return ErrNotFound
    }
    m.users[user.ID] = user
    return nil
}
​
func (m *MockUserRepository) DeleteUser(id int) error {
    if m.err != nil {
        return m.err
    }
    if _, ok := m.users[id]; !ok {
        return ErrNotFound
    }
    delete(m.users, id)
    return nil
}
​
func (m *MockUserRepository) ListUsers() ([]User, error) {
    if m.err != nil {
        return nil, m.err
    }
    users := make([]User, 0, len(m.users))
    for _, user := range m.users {
        users = append(users, *user)
    }
    return users, nil
}
​
// 设置错误模拟
func (m *MockUserRepository) SetError(err error) {
    m.err = err
}

6.2 使用Mock进行测试

复制代码
package service
​
import (
    "testing"
)
​
func TestUserService(t *testing.T) {
    mockRepo := NewMockUserRepository()
    service := NewUserService(mockRepo)
​
    t.Run("GetUser", func(t *testing.T) {
        // 准备数据
        mockRepo.users[1] = &User{ID: 1, Name: "Alice", Email: "alice@example.com"}
​
        // 执行
        user, err := service.GetUser(1)
​
        // 断言
        if err != nil {
            t.Fatalf("Expected no error, got %v", err)
        }
        if user.Name != "Alice" {
            t.Errorf("Expected name Alice, got %s", user.Name)
        }
    })
​
    t.Run("GetUserNotFound", func(t *testing.T) {
        user, err := service.GetUser(999)
        if err != ErrNotFound {
            t.Errorf("Expected ErrNotFound, got %v", err)
        }
        if user != nil {
            t.Error("Expected nil user for not found")
        }
    })
​
    t.Run("CreateUser", func(t *testing.T) {
        user := &User{Name: "Bob", Email: "bob@example.com"}
        err := service.CreateUser(user)
        if err != nil {
            t.Fatalf("CreateUser failed: %v", err)
        }
        if user.ID == 0 {
            t.Error("User ID should be set after creation")
        }
    })
​
    t.Run("CreateUserWithError", func(t *testing.T) {
        mockRepo.SetError(ErrInvalidInput)
        err := service.CreateUser(&User{Name: ""})
        if err != ErrInvalidInput {
            t.Errorf("Expected ErrInvalidInput, got %v", err)
        }
        mockRepo.SetError(nil)
    })
}

6.3 使用httptest进行HTTP测试

复制代码
package handler
​
import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)
​
func TestUserHandler(t *testing.T) {
    mux := http.NewServeMux()
    handler := &UserHandler{}
    mux.HandleFunc("/users", handler.ServeHTTP)
​
    t.Run("GET /users", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/users", nil)
        rr := httptest.NewRecorder()
​
        mux.ServeHTTP(rr, req)
​
        if rr.Code != http.StatusOK {
            t.Errorf("Expected status 200, got %d", rr.Code)
        }
​
        var users []User
        if err := json.NewDecoder(rr.Body).Decode(&users); err != nil {
            t.Fatalf("Failed to decode response: %v", err)
        }
    })
​
    t.Run("POST /users", func(t *testing.T) {
        body := `{"name":"Charlie","email":"charlie@example.com"}`
        req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        rr := httptest.NewRecorder()
​
        mux.ServeHTTP(rr, req)
​
        if rr.Code != http.StatusCreated {
            t.Errorf("Expected status 201, got %d", rr.Code)
        }
​
        var user User
        if err := json.NewDecoder(rr.Body).Decode(&user); err != nil {
            t.Fatalf("Failed to decode response: %v", err)
        }
​
        if user.Name != "Charlie" {
            t.Errorf("Expected name Charlie, got %s", user.Name)
        }
    })
​
    t.Run("POST /users with invalid data", func(t *testing.T) {
        body := `{"name":"","email":"invalid"}`
        req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        rr := httptest.NewRecorder()
​
        mux.ServeHTTP(rr, req)
​
        if rr.Code != http.StatusBadRequest {
            t.Errorf("Expected status 400, got %d", rr.Code)
        }
    })
}

6.4 使用 testify/assert

复制代码
package example
​
import (
    "testing"
​
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)
​
func TestWithTestify(t *testing.T) {
    t.Run("assertions", func(t *testing.T) {
        assert.Equal(t, 4, 2+2, "Math should work")
        assert.NotNil(t, new(int))
        assert.True(t, true)
        assert.Contains(t, "hello world", "world")
    })
​
    t.Run("require", func(t *testing.T) {
        // require在失败时立即终止测试
        require.NoError(t, nil)
        require.Equal(t, 1, 1)
    })
}
​
func TestSliceContain(t *testing.T) {
    assert.ElementsMatch(t, []int{1, 2, 3}, []int{3, 2, 1})
    assert.Subset(t, []int{1, 2, 3, 4}, []int{1, 2})
}

七、实际案例:测试驱动开发实践

7.1 TDD循环

测试驱动开发遵循"红-绿-重构"循环:

复制代码
// 第一步:编写一个失败的测试(红)
func TestStack(t *testing.T) {
    s := NewStack()
​
    // 测试空栈pop应该返回错误
    _, err := s.Pop()
    if err == nil {
        t.Error("Expected error when popping from empty stack")
    }
}
​
// 第二步:编写最小实现使测试通过(绿)
type Stack struct {
    items []int
}
​
func NewStack() *Stack {
    return &Stack{items: make([]int, 0)}
}
​
func (s *Stack) Pop() (int, error) {
    if len(s.items) == 0 {
        return 0, errors.New("stack is empty")
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, nil
}
​
// 第三步:重构

7.2 完整TDD示例:缓存实现

复制代码
package cache
​
import (
    "errors"
    "sync"
    "time"
)
​
var (
    ErrKeyNotFound = errors.New("key not found")
    ErrKeyExpired  = errors.New("key expired")
)
​
type Item struct {
    Value      interface{}
    Expiration int64 // 过期时间戳,0表示永不过期
}
​
func (i *Item) IsExpired() bool {
    if i.Expiration == 0 {
        return false
    }
    return time.Now().UnixNano() > i.Expiration
}
​
type Cache struct {
    items map[string]*Item
    mu    sync.RWMutex
}
​
func New() *Cache {
    return &Cache{
        items: make(map[string]*Item),
    }
}
​
// Test: 设置和获取值
func TestCacheSetGet(t *testing.T) {
    cache := New()
    cache.Set("key1", "value1", 0)
​
    value, err := cache.Get("key1")
    if err != nil {
        t.Fatalf("Unexpected error: %v", err)
    }
    if value != "value1" {
        t.Errorf("Expected value1, got %v", value)
    }
}
​
// Test: 获取不存在的key
func TestCacheGetNotFound(t *testing.T) {
    cache := New()
​
    _, err := cache.Get("nonexistent")
    if !errors.Is(err, ErrKeyNotFound) {
        t.Errorf("Expected ErrKeyNotFound, got %v", err)
    }
}
​
// Test: 删除key
func TestCacheDelete(t *testing.T) {
    cache := New()
    cache.Set("key1", "value1", 0)
​
    err := cache.Delete("key1")
    if err != nil {
        t.Fatalf("Delete failed: %v", err)
    }
​
    _, err = cache.Get("key1")
    if !errors.Is(err, ErrKeyNotFound) {
        t.Errorf("Expected ErrKeyNotFound after delete, got %v", err)
    }
}
​
// 实现
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
​
    expiration := int64(0)
    if ttl > 0 {
        expiration = time.Now().Add(ttl).UnixNano()
    }
​
    c.items[key] = &Item{
        Value:      value,
        Expiration: expiration,
    }
}
​
func (c *Cache) Get(key string) (interface{}, error) {
    c.mu.RLock()
    defer c.mu.RUnlock()
​
    item, ok := c.items[key]
    if !ok {
        return nil, ErrKeyNotFound
    }
​
    if item.IsExpired() {
        return nil, ErrKeyExpired
    }
​
    return item.Value, nil
}
​
func (c *Cache) Delete(key string) error {
    c.mu.Lock()
    defer c.mu.Unlock()
​
    if _, ok := c.items[key]; !ok {
        return ErrKeyNotFound
    }
​
    delete(c.items, key)
    return nil
}

7.3 基准测试缓存实现

复制代码
func BenchmarkCacheSet(b *testing.B) {
    cache := New()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        cache.Set("key", "value", 0)
    }
}
​
func BenchmarkCacheGet(b *testing.B) {
    cache := New()
    cache.Set("key", "value", 0)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = cache.Get("key")
    }
}
​
func BenchmarkCacheConcurrency(b *testing.B) {
    cache := New()
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            cache.Set("key", "value", 0)
            _, _ = cache.Get("key")
        }
    })
}

7.4 集成测试

复制代码
package integration
​
import (
    "database/sql"
    "net/http"
    "net/http/httptest"
    "os"
    "testing"
​
    "mypackage/handler"
    "mypackage/repository"
    "mypackage/service"
​
    _ "github.com/go-sql-driver/mysql"
)
​
var (
    testDB   *sql.DB
    testMux  *http.ServeMux
)
​
func TestMain(m *testing.M) {
    // Setup
    var err error
    testDB, err = sql.Open("mysql", os.Getenv("TEST_DB_URL"))
    if err != nil {
        panic(err)
    }
​
    // 创建测试服务
    userRepo := repository.NewUserRepository(testDB)
    userSvc := service.NewUserService(userRepo)
    userHandler := handler.NewUserHandler(userSvc)
    testMux = http.NewServeMux()
    testMux.HandleFunc("/api/users", userHandler.ServeHTTP)
​
    // 运行测试
    exitCode := m.Run()
​
    // Teardown
    testDB.Close()
​
    os.Exit(exitCode)
}
​
func TestCreateUser(t *testing.T) {
    reqBody := `{"username":"testuser","email":"test@example.com"}`
    req := httptest.NewRequest(http.MethodPost, "/api/users", strings.NewReader(reqBody))
    req.Header.Set("Content-Type", "application/json")
    rr := httptest.NewRecorder()
​
    testMux.ServeHTTP(rr, req)
​
    if rr.Code != http.StatusCreated {
        t.Errorf("Expected status 201, got %d", rr.Code)
    }
}

总结

本文全面介绍了Go语言测试的各个方面:

  1. 单元测试规范 :掌握_test.go命名约定、Test函数签名和基本断言编写。

  2. 测试夹具 :使用TestMain实现包级Setup/Teardown,在每个测试中准备和清理数据。

  3. 子测试 :使用t.Run组织相关测试,实现测试并行化和选择性运行。

  4. 基准测试 :使用Benchmark函数编写性能测试,理解b.N的运行机制。

  5. 覆盖率分析 :使用go test -coverprofile生成覆盖率报告,优化测试覆盖。

  6. Mock技术 :通过接口抽象和Mock实现解耦测试,使用httptest测试HTTP处理器。

  7. 测试驱动开发:遵循红-绿-重构循环,从测试出发设计代码。

测试不仅是质量保证的手段,更是代码设计的指南。通过良好的测试实践,可以构建更健壮、更易维护的软件系统。

相关推荐
chxii1 小时前
lua流程控制语句和table(表)数据结构
开发语言·junit·lua
逻辑驱动的ken2 小时前
Java高频面试考点场景题20
java·开发语言·深度学习·面试·职场和发展
W.A委员会2 小时前
多行溢出在末尾添加省略号
开发语言·javascript·css
wjs20242 小时前
RSS Item 元素:深入解析与使用指南
开发语言
小郑加油2 小时前
python学习Day11:认识与创建CSV文件
开发语言·python·学习
念何架构之路2 小时前
Go Web基础和Http演进
开发语言·后端·golang
初心未改HD2 小时前
Go语言database/sql与SQLx:构建健壮的数据访问层
开发语言·golang
晚风吹红霞2 小时前
C++异常处理核心知识点全解析
开发语言·c++
CoderCodingNo2 小时前
【信奥业余科普】C++ 的奇妙之旅 | 17:面的铺展与文本的本质——二维数组与字符串
开发语言·c++