GO语言测试最佳实践
# GO语言测试最佳实践
# 1. 测试基础
# 1.1 测试文件命名
测试文件以_test.go
结尾,与被测试文件在同一目录下。
// calculator.go
package math
func Add(a, b int) int {
return a + b
}
// calculator_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
}
}
# 1.2 运行测试
# 运行当前包的测试
go test
# 运行特定测试
go test -run TestAdd
# 运行测试并显示详细信息
go test -v
# 运行测试并显示覆盖率
go test -cover
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
# 2. 测试函数
# 2.1 基本测试函数
func TestFunctionName(t *testing.T) {
// 测试代码
}
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -2, -3},
{"zero", 0, 5, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; expected %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
# 2.2 基准测试
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
func BenchmarkAddParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(1, 2)
}
})
}
# 2.3 示例测试
func ExampleAdd() {
result := Add(2, 3)
fmt.Println(result)
// Output: 5
}
func ExampleAdd_multiple() {
fmt.Println(Add(1, 2))
fmt.Println(Add(-1, 1))
fmt.Println(Add(0, 0))
// Output:
// 3
// 0
// 0
}
# 3. 测试工具
# 3.1 使用testing.T
func TestWithHelpers(t *testing.T) {
// 跳过测试
if testing.Short() {
t.Skip("Skipping test in short mode")
}
// 标记测试失败但不停止
t.Log("This is a log message")
t.Error("This is an error but test continues")
// 标记测试失败并停止
t.Fatal("This stops the test")
}
# 3.2 使用testing.B
func BenchmarkWithSetup(b *testing.B) {
// 设置代码,只运行一次
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer() // 重置计时器
for i := 0; i < b.N; i++ {
// 被测试的代码
processData(data)
}
}
# 3.3 使用testing.M
func TestMain(m *testing.M) {
// 设置代码
setup()
// 运行测试
code := m.Run()
// 清理代码
cleanup()
os.Exit(code)
}
# 4. 测试模式
# 4.1 表驱动测试
func TestMultiply(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 6},
{"negative", -2, 3, -6},
{"zero", 0, 5, 0},
{"large", 1000, 1000, 1000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Multiply(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Multiply(%d, %d) = %d; expected %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
# 4.2 子测试
func TestStringOperations(t *testing.T) {
t.Run("uppercase", func(t *testing.T) {
result := ToUpper("hello")
expected := "HELLO"
if result != expected {
t.Errorf("ToUpper('hello') = %s; expected %s", result, expected)
}
})
t.Run("lowercase", func(t *testing.T) {
result := ToLower("WORLD")
expected := "world"
if result != expected {
t.Errorf("ToLower('WORLD') = %s; expected %s", result, expected)
}
})
}
# 4.3 测试套件
type CalculatorTestSuite struct {
suite.Suite
calc *Calculator
}
func (suite *CalculatorTestSuite) SetupTest() {
suite.calc = NewCalculator()
}
func (suite *CalculatorTestSuite) TestAdd() {
result := suite.calc.Add(2, 3)
suite.Equal(5, result)
}
func (suite *CalculatorTestSuite) TestSubtract() {
result := suite.calc.Subtract(5, 3)
suite.Equal(2, result)
}
func TestCalculatorTestSuite(t *testing.T) {
suite.Run(t, new(CalculatorTestSuite))
}
# 5. 模拟和存根
# 5.1 接口模拟
type Database interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}
type MockDatabase struct {
users map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
user, exists := m.users[id]
if !exists {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func (m *MockDatabase) SaveUser(user *User) error {
m.users[user.ID] = user
return nil
}
func TestUserService(t *testing.T) {
mockDB := &MockDatabase{
users: make(map[int]*User),
}
service := NewUserService(mockDB)
// 测试代码
user, err := service.GetUser(1)
if err == nil {
t.Error("Expected error for non-existent user")
}
}
# 5.2 使用gomock
//go:generate mockgen -source=database.go -destination=mock_database.go -package=mocks
type Database interface {
GetUser(id int) (*User, error)
}
func TestWithGomock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDB := NewMockDatabase(ctrl)
mockDB.EXPECT().GetUser(1).Return(&User{ID: 1, Name: "Alice"}, nil)
service := NewUserService(mockDB)
user, err := service.GetUser(1)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if user.Name != "Alice" {
t.Errorf("Expected Alice, got %s", user.Name)
}
}
# 6. 测试HTTP服务
# 6.1 使用httptest
func TestHTTPHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/user/1", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(UserHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := `{"id":1,"name":"Alice"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
# 6.2 测试HTTP客户端
func TestHTTPClient(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id":1,"name":"Alice"}`))
}))
defer server.Close()
resp, err := http.Get(server.URL + "/user/1")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
}
# 7. 测试数据库
# 7.1 使用测试数据库
func TestDatabaseOperations(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
// 创建表
_, err = db.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)
`)
if err != nil {
t.Fatal(err)
}
// 测试插入
_, err = db.Exec("INSERT INTO users (id, name) VALUES (?, ?)", 1, "Alice")
if err != nil {
t.Errorf("Failed to insert user: %v", err)
}
// 测试查询
var name string
err = db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)
if err != nil {
t.Errorf("Failed to query user: %v", err)
}
if name != "Alice" {
t.Errorf("Expected Alice, got %s", name)
}
}
# 8. 性能测试
# 8.1 基准测试
func BenchmarkStringConcatenation(b *testing.B) {
b.Run("plus", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "Hello" + " " + "World"
}
})
b.Run("fmt", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s %s", "Hello", "World")
}
})
b.Run("strings.Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(" ")
builder.WriteString("World")
_ = builder.String()
}
})
}
# 8.2 内存分析
func BenchmarkMemoryAllocation(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
data := make([]int, 1000)
for j := range data {
data[j] = j
}
}
}
# 9. 测试最佳实践
# 9.1 测试命名
// 好的命名
func TestAdd_WithPositiveNumbers_ReturnsSum(t *testing.T) { }
func TestAdd_WithNegativeNumbers_ReturnsSum(t *testing.T) { }
func TestAdd_WithZero_ReturnsOtherNumber(t *testing.T) { }
// 避免的命名
func TestAdd1(t *testing.T) { }
func TestAdd2(t *testing.T) { }
# 9.2 测试组织
// 按功能组织测试
func TestUserService_CreateUser(t *testing.T) { }
func TestUserService_GetUser(t *testing.T) { }
func TestUserService_UpdateUser(t *testing.T) { }
func TestUserService_DeleteUser(t *testing.T) { }
# 9.3 测试数据
// 使用常量定义测试数据
const (
testUserID = 1
testUserName = "Alice"
testUserEmail = "alice@example.com"
)
func TestUserService(t *testing.T) {
user := &User{
ID: testUserID,
Name: testUserName,
Email: testUserEmail,
}
// 测试代码
}
# 9.4 测试清理
func TestWithCleanup(t *testing.T) {
// 设置
tempDir := t.TempDir()
file := filepath.Join(tempDir, "test.txt")
// 测试完成后自动清理
t.Cleanup(func() {
// 额外的清理工作
})
// 测试代码
err := os.WriteFile(file, []byte("test"), 0644)
if err != nil {
t.Fatal(err)
}
}
# 10. 测试覆盖率
# 10.1 覆盖率目标
// 设置覆盖率目标
func TestMain(m *testing.M) {
// 运行测试
code := m.Run()
// 检查覆盖率
if testing.CoverMode() != "" {
coverage := testing.Coverage()
if coverage < 0.8 {
fmt.Printf("Coverage %.2f%% is below 80%%\n", coverage*100)
os.Exit(1)
}
}
os.Exit(code)
}
# 10.2 生成覆盖率报告
# 生成覆盖率文件
go test -coverprofile=coverage.out
# 查看HTML报告
go tool cover -html=coverage.out -o coverage.html
# 查看函数覆盖率
go tool cover -func=coverage.out