引言
测试是保证软件质量的重要手段。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语言测试的各个方面:
-
单元测试规范 :掌握
_test.go命名约定、Test函数签名和基本断言编写。 -
测试夹具 :使用
TestMain实现包级Setup/Teardown,在每个测试中准备和清理数据。 -
子测试 :使用
t.Run组织相关测试,实现测试并行化和选择性运行。 -
基准测试 :使用
Benchmark函数编写性能测试,理解b.N的运行机制。 -
覆盖率分析 :使用
go test -coverprofile生成覆盖率报告,优化测试覆盖。 -
Mock技术 :通过接口抽象和Mock实现解耦测试,使用
httptest测试HTTP处理器。 -
测试驱动开发:遵循红-绿-重构循环,从测试出发设计代码。
测试不仅是质量保证的手段,更是代码设计的指南。通过良好的测试实践,可以构建更健壮、更易维护的软件系统。