1.7.1 单元测试基础 #
单元测试是软件测试的基础,它专注于测试代码中的最小可测试单元。Go 语言内置了完整的测试框架,使得编写和运行测试变得简单而直观。
Go 测试框架概述 #
Go 的测试框架基于以下几个核心概念:
- 测试文件 - 以
_test.go
结尾的文件 - 测试函数 - 以
Test
开头的函数 - testing 包 - 提供测试相关的工具和断言
- go test 命令 - 运行测试的命令行工具
基本测试结构 #
让我们从一个简单的例子开始:
// math_utils.go
package main
// Add 计算两个整数的和
func Add(a, b int) int {
return a + b
}
// Multiply 计算两个整数的乘积
func Multiply(a, b int) int {
return a * b
}
// Divide 计算两个数的除法,返回结果和错误
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
对应的测试文件:
// math_utils_test.go
package main
import (
"testing"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
func TestMultiply(t *testing.T) {
result := Multiply(4, 5)
expected := 20
if result != expected {
t.Errorf("Multiply(4, 5) = %d; want %d", result, expected)
}
}
func TestDivide(t *testing.T) {
// 测试正常情况
result, err := Divide(10, 2)
if err != nil {
t.Errorf("Divide(10, 2) returned error: %v", err)
}
expected := 5.0
if result != expected {
t.Errorf("Divide(10, 2) = %f; want %f", result, expected)
}
// 测试除零错误
_, err = Divide(10, 0)
if err == nil {
t.Error("Divide(10, 0) should return error")
}
}
测试函数的命名规则 #
测试函数必须遵循特定的命名规则:
- 函数名 - 必须以
Test
开头 - 参数 - 必须接受
*testing.T
类型的参数 - 可见性 - 函数名首字母必须大写(公开函数)
// 正确的测试函数命名
func TestUserRegistration(t *testing.T) { /* ... */ }
func TestEmailValidation(t *testing.T) { /* ... */ }
func TestPasswordEncryption(t *testing.T) { /* ... */ }
// 错误的命名(不会被识别为测试函数)
func testUserLogin(t *testing.T) { /* ... */ } // 首字母小写
func UserTest(t *testing.T) { /* ... */ } // 不以Test开头
func TestUser() { /* ... */ } // 缺少参数
testing.T 类型的常用方法 #
testing.T
提供了丰富的方法来处理测试结果:
错误报告方法 #
func TestErrorMethods(t *testing.T) {
// Error 和 Errorf - 报告错误但继续执行
if condition {
t.Error("Something went wrong")
t.Errorf("Expected %d, got %d", expected, actual)
}
// Fatal 和 Fatalf - 报告错误并立即停止测试
if criticalCondition {
t.Fatal("Critical error occurred")
t.Fatalf("Cannot continue: %v", err)
}
// Fail 和 FailNow - 标记测试失败
if shouldFail {
t.Fail() // 标记失败但继续执行
t.FailNow() // 标记失败并立即停止
}
}
日志输出方法 #
func TestLoggingMethods(t *testing.T) {
// Log 和 Logf - 输出日志信息
t.Log("This is a log message")
t.Logf("Processing item %d of %d", i, total)
// 只有在测试失败或使用 -v 标志时才会显示日志
}
跳过测试 #
func TestSkipExample(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping test on Windows")
}
// 条件跳过
if !hasRequiredFeature() {
t.Skipf("Skipping test: required feature not available")
}
// 测试代码...
}
表格驱动测试 #
表格驱动测试是 Go 中常用的测试模式,特别适合测试多个输入输出组合:
func TestAddTableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"mixed numbers", -5, 10, 5},
{"zero values", 0, 0, 0},
{"with zero", 5, 0, 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; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
复杂数据结构的测试 #
type User struct {
ID int
Name string
Email string
Age int
}
func ValidateUser(user User) error {
if user.Name == "" {
return errors.New("name cannot be empty")
}
if user.Age < 0 {
return errors.New("age cannot be negative")
}
if !strings.Contains(user.Email, "@") {
return errors.New("invalid email format")
}
return nil
}
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user User
wantErr bool
errMsg string
}{
{
name: "valid user",
user: User{ID: 1, Name: "John", Email: "[email protected]", Age: 25},
wantErr: false,
},
{
name: "empty name",
user: User{ID: 2, Name: "", Email: "[email protected]", Age: 30},
wantErr: true,
errMsg: "name cannot be empty",
},
{
name: "negative age",
user: User{ID: 3, Name: "Jane", Email: "[email protected]", Age: -5},
wantErr: true,
errMsg: "age cannot be negative",
},
{
name: "invalid email",
user: User{ID: 4, Name: "Bob", Email: "invalid-email", Age: 20},
wantErr: true,
errMsg: "invalid email format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.user)
if tt.wantErr {
if err == nil {
t.Errorf("ValidateUser() error = nil, wantErr %v", tt.wantErr)
return
}
if err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error = %v, want %v", err.Error(), tt.errMsg)
}
} else {
if err != nil {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
}
}
})
}
}
子测试和并行测试 #
子测试 #
使用 t.Run()
可以创建子测试,提供更好的测试组织和错误报告:
func TestStringOperations(t *testing.T) {
t.Run("ToUpper", func(t *testing.T) {
result := strings.ToUpper("hello")
if result != "HELLO" {
t.Errorf("ToUpper failed: got %s, want HELLO", result)
}
})
t.Run("ToLower", func(t *testing.T) {
result := strings.ToLower("WORLD")
if result != "world" {
t.Errorf("ToLower failed: got %s, want world", result)
}
})
t.Run("Contains", func(t *testing.T) {
if !strings.Contains("hello world", "world") {
t.Error("Contains failed: should find 'world' in 'hello world'")
}
})
}
并行测试 #
使用 t.Parallel()
可以让测试并行运行,提高测试执行效率:
func TestParallelOperations(t *testing.T) {
t.Run("Operation1", func(t *testing.T) {
t.Parallel()
// 这个测试会并行运行
time.Sleep(100 * time.Millisecond)
// 测试逻辑...
})
t.Run("Operation2", func(t *testing.T) {
t.Parallel()
// 这个测试也会并行运行
time.Sleep(100 * time.Millisecond)
// 测试逻辑...
})
}
测试辅助函数 #
创建辅助函数可以减少测试代码的重复:
// 测试辅助函数
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper() // 标记为辅助函数,错误报告会指向调用者
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func assertError(t *testing.T, err error, wantErr bool) {
t.Helper()
if (err != nil) != wantErr {
t.Errorf("error = %v, wantErr %v", err, wantErr)
}
}
// 使用辅助函数的测试
func TestWithHelpers(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5)
_, err := Divide(10, 0)
assertError(t, err, true)
}
运行测试 #
基本命令 #
# 运行当前包的所有测试
go test
# 运行指定包的测试
go test ./package/path
# 运行所有包的测试
go test ./...
# 显示详细输出
go test -v
# 运行特定的测试函数
go test -run TestAdd
# 运行匹配模式的测试
go test -run "TestAdd|TestMultiply"
高级选项 #
# 显示测试覆盖率
go test -cover
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
# 并行运行测试
go test -parallel 4
# 设置超时时间
go test -timeout 30s
# 运行基准测试
go test -bench=.
# 多次运行测试检查稳定性
go test -count=10
测试最佳实践 #
1. 测试命名 #
// 好的测试命名 - 描述性强
func TestUserService_CreateUser_WithValidData_ReturnsUser(t *testing.T) {}
func TestEmailValidator_ValidateEmail_WithInvalidFormat_ReturnsError(t *testing.T) {}
// 避免的命名 - 过于简单
func TestUser(t *testing.T) {}
func TestValidate(t *testing.T) {}
2. 测试组织 #
func TestUserService(t *testing.T) {
// 使用子测试组织相关测试
t.Run("CreateUser", func(t *testing.T) {
t.Run("WithValidData", func(t *testing.T) {
// 测试逻辑
})
t.Run("WithInvalidData", func(t *testing.T) {
// 测试逻辑
})
})
t.Run("UpdateUser", func(t *testing.T) {
// 更新用户的测试
})
}
3. 测试数据准备 #
func TestUserOperations(t *testing.T) {
// 准备测试数据
validUser := User{
ID: 1,
Name: "Test User",
Email: "[email protected]",
Age: 25,
}
// 使用表格驱动测试处理多种情况
tests := []struct {
name string
user User
want bool
}{
{"valid user", validUser, true},
{"invalid user", User{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试逻辑
})
}
}
4. 错误处理测试 #
func TestErrorHandling(t *testing.T) {
// 测试错误情况
_, err := SomeFunction("invalid input")
if err == nil {
t.Error("Expected error for invalid input, got nil")
}
// 测试特定错误类型
var expectedErr *CustomError
if !errors.As(err, &expectedErr) {
t.Errorf("Expected CustomError, got %T", err)
}
}
通过掌握这些单元测试基础知识,您已经具备了编写高质量测试代码的能力。在下一节中,我们将学习如何使用测试覆盖率工具来评估测试质量,以及如何编写性能基准测试。