Go 单元测试全面指南
1. 单元测试基础概念
单元测试是对软件中最小可测试单元(通常是函数或方法)进行检查和验证的过程。在 Go 中,单元测试有以下特点:
- 测试文件以 _test.go 结尾
- 测试函数以 Test 开头
- 测试函数接收 *testing.T 参数
- 使用 go test 命令运行测试
2. 测试文件结构
一个典型的 Go 测试文件结构如下:
project/
├── main.go # 源代码
└── main_test.go # 测试代码3. 基本测试示例
被测代码 (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"
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 TestSubtract(t *testing.T) {
result := Subtract(5, 3)
expected := 2
if result != expected {
t.Errorf("Subtract(5, 3) = %d; want %d", result, expected)
}
}4. 测试运行方式
# 运行所有测试
go test
# 显示详细输出
go test -v
# 运行特定测试
go test -v -run TestAdd
# 运行测试并显示覆盖率
go test -cover
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out5. 表驱动测试
表驱动测试是一种将测试用例组织成表格形式的测试方法:
func TestAdd_TableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -1, -2},
{"mixed numbers", -1, 1, 0},
{"zero values", 0, 0, 0},
}
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)
}
})
}
}6. 子测试与并行测试
func TestMultiply(t *testing.T) {
t.Parallel() // 标记测试可以并行运行
t.Run("positive numbers", func(t *testing.T) {
if Multiply(2, 3) != 6 {
t.Error("expected 6")
}
})
t.Run("negative numbers", func(t *testing.T) {
if Multiply(-2, -3) != 6 {
t.Error("expected 6")
}
})
}7. 测试辅助函数
func assertEqual(t *testing.T, result, expected int) {
t.Helper() // 标记为辅助函数,错误报告会跳过此函数
if result != expected {
t.Errorf("got %d, want %d", result, expected)
}
}
func TestDivide(t *testing.T) {
assertEqual(t, Divide(6, 3), 2)
assertEqual(t, Divide(10, 2), 5)
}8. 基准测试
基准测试用于测量代码性能:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}运行基准测试:
go test -bench=.
go test -bench=. -benchmem # 包含内存分配信息9. 示例测试
示例测试既作为测试也作为文档:
func ExampleAdd() {
sum := Add(1, 2)
fmt.Println(sum)
// Output: 3
}10. 测试初始化与清理
func TestMain(m *testing.M) {
// 测试前初始化
setup()
// 运行测试
code := m.Run()
// 测试后清理
teardown()
// 退出
os.Exit(code)
}
func setup() {
fmt.Println("测试初始化")
}
func teardown() {
fmt.Println("测试清理")
}11. 测试替身(Mock/Stub)
使用接口实现测试替身:
type Database interface {
GetUser(id int) (string, error)
}
type RealDB struct{}
func (db *RealDB) GetUser(id int) (string, error) {
// 实际数据库操作
return "real user", nil
}
type MockDB struct{}
func (db *MockDB) GetUser(id int) (string, error) {
return "mock user", nil
}
func TestGetUserName(t *testing.T) {
db := &MockDB{}
name, _ := db.GetUser(1)
if name != "mock user" {
t.Errorf("expected mock user, got %s", name)
}
}12. 测试HTTP服务
func TestHTTPHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/hello", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(HelloHandler)
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 := "Hello, World!"
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}13. 测试命令行工具
func TestCLI(t *testing.T) {
cmd := exec.Command("./myapp", "arg1", "arg2")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("command failed: %v\n%s", err, output)
}
expected := "expected output"
if !strings.Contains(string(output), expected) {
t.Errorf("unexpected output: got %q, want %q", string(output), expected)
}
}14. 测试覆盖率
# 生成覆盖率文件
go test -coverprofile=coverage.out
# 查看覆盖率报告
go tool cover -func=coverage.out
# 以HTML形式查看
go tool cover -html=coverage.out
# 测试覆盖率阈值检查
go test -cover -covermode=count -coverpkg=./... -coverprofile=coverage.out
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//'15. 高级测试技巧
15.1 测试时间相关代码
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
// 长时间运行的测试代码
}运行短测试:
go test -short15.2 测试随机行为
func TestRandomBehavior(t *testing.T) {
rand.Seed(1) // 固定随机种子以获得可重复的结果
// 测试代码
}15.3 测试私有函数
虽然不推荐直接测试私有函数,但可以通过以下方式实现:
// 在测试文件中导出私有函数进行测试
var ExportedPrivateFunc = privateFunc
func TestPrivateFunc(t *testing.T) {
result := ExportedPrivateFunc()
// 断言
}16. 测试最佳实践
- 保持测试独立:每个测试应该独立运行,不依赖其他测试的状态
- 测试命名清晰:使用描述性的测试名称
- 测试失败信息明确:错误信息应该清楚地说明期望值和实际值
- 避免测试实现细节:测试行为而非实现
- 保持测试快速:单元测试应该快速执行
- 覆盖率合理:追求有意义的覆盖率而非100%
- 测试边界条件:包括零值、空值、最大值、最小值等
- 定期重构测试:随着代码演进,测试也需要维护
通过掌握这些单元测试技术,你可以为 Go 项目构建可靠的测试套件,确保代码质量并支持持续集成和部署流程。