21 - 测试:testing 包、表驱动测试、Mock、TestMain
21 - 测试
21.1 基本测试
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d, want %d", got, want)
}
}
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5.0 {
t.Errorf("Divide(10, 2) = %f, want 5.0", result)
}
}
func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("expected error for division by zero")
}
}
| 函数 | 作用 |
|---|
t.Error() | 报告错误,继续执行 |
t.Errorf() | 格式化报告错误 |
t.Fatal() | 报告错误,立即停止当前测试 |
t.Fatalf() | 格式化报告错误并停止 |
t.Skip() | 跳过当前测试 |
t.Log() | 记录日志(仅 -v 时显示) |
t.Run() | 运行子测试 |
21.2 表驱动测试(Table-Driven Tests)
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"正数相加", 2, 3, 5},
{"负数相加", -2, -3, -5},
{"零值", 0, 0, 0},
{"正负相加", 5, -3, 2},
{"大数", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
21.3 测试辅助函数
func TestWithHelper(t *testing.T) {
t.Helper() // 标记为辅助函数,错误位置报告到调用者
assertEqual := func(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
assertEqual(t, Add(2, 3), 5)
assertEqual(t, Add(0, 0), 0)
}
func TestWithError(t *testing.T) {
assertError := func(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Error("expected error, got nil")
}
}
assertNoError := func(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
_, err := Divide(10, 0)
assertError(t, err)
_, err = Divide(10, 2)
assertNoError(t, err)
}
21.4 TestMain
func TestMain(m *testing.M) {
// 测试前设置
setup()
// 运行所有测试
code := m.Run()
// 测试后清理
teardown()
os.Exit(code)
}
func setup() {
// 初始化测试数据库、启动服务等
}
func teardown() {
// 清理资源
}
21.5 Mock 和接口测试
// 定义接口
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// 真实实现
type PostgresUserRepo struct {
db *sql.DB
}
func (r *PostgresUserRepo) FindByID(id int) (*User, error) {
// SQL 查询...
}
// Mock 实现
type MockUserRepo struct {
users map[int]*User
err error
}
func (m *MockUserRepo) FindByID(id int) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *MockUserRepo) Save(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// 测试服务层
func TestUserService_GetUser(t *testing.T) {
mock := &MockUserRepo{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := NewUserService(mock)
user, err := service.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got name %s, want Alice", user.Name)
}
}
21.6 并发测试
func TestConcurrent(t *testing.T) {
counter := 0
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}
wg.Wait()
if counter != 1000 {
t.Errorf("counter = %d, want 1000", counter)
}
}
// 使用 -race 检测竞态
// go test -race ./...
21.7 测试覆盖率
# 生成覆盖率报告
go test -cover ./...
# 生成详细报告
go test -coverprofile=coverage.out ./...
# 查看覆盖率
go tool cover -func=coverage.out
# 生成 HTML 报告
go tool cover -html=coverage.out -o coverage.html
🏢 业务场景
- 单元测试:测试函数、方法的正确性
- 集成测试:TestMain 初始化数据库,测试完整流程
- 接口测试:Mock 外部依赖,测试业务逻辑
- 回归测试:表驱动测试覆盖边界条件
📖 扩展阅读