Go 测试:让代码更可靠的秘密武器

系统讲解Go语言测试技术,涵盖单元测试、基准测试、并发测试、Mock测试和模糊测试等内容,帮助开发者建立可靠的代码质量保障体系

Go 测试:让代码更可靠的秘密武器

“我的代码能跑就行,为什么要写测试?”

如果你有过这样的想法,那你还没经历过这样的场景:凌晨三点,客户报告生产环境出了一个 bug,你翻遍了代码也找不到问题在哪;或者你修改了一个看似无关的函数,结果整个系统都崩溃了。

写测试不是为了凑代码行数,而是为了让你晚上能睡个好觉。

Go 语言把测试放在了非常重要的位置——testing 是标准库的一部分,go test 命令内置在工具链中。这意味着你不需要安装任何第三方框架就能写测试。今天我们就来全面学习 Go 的测试技术。

第一个测试

Go 的测试文件有严格的命名规则:

  • 测试文件以 _test.go 结尾
  • 测试函数以 Test 开头,参数是 *testing.T

来看一个简单的例子。假设我们有一个 mathutil 包:

// mathutil.go
package mathutil

func Add(a, b int) int {
	return a + b
}

func Max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

对应的测试文件:

// mathutil_test.go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
	result := Add(2, 3)
	if result != 5 {
		t.Errorf("Add(2, 3) = %d; want 5", result)
	}
}

func TestMax(t *testing.T) {
	result := Max(5, 3)
	if result != 5 {
		t.Errorf("Max(5, 3) = %d; want 5", result)
	}
}

运行测试:

$ go test
PASS
ok  	example/mathutil	0.001s

如果要看到详细输出:

$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestMax
--- PASS: TestMax (0.00s)
PASS
ok  	example/mathutil	0.001s

测试失败时的报告

让我们看一个失败的测试:

func TestAddFail(t *testing.T) {
	result := Add(2, 3)
	if result != 6 {
		t.Errorf("Add(2, 3) = %d; want 6", result)
	}
}

输出:

--- FAIL: TestAddFail (0.00s)
    mathutil_test.go:10: Add(2, 3) = 5; want 6
FAIL

注意 t.Errorf 和 t.Fatalf 的区别:

  • t.Errorf:报告错误但继续执行
  • t.Fatalf:报告错误并立即终止测试

表驱动测试(Table-Driven Tests)

这是 Go 社区强烈推荐的测试模式。把多个测试用例组织成表格,用循环执行:

func TestMax(t *testing.T) {
	tests := []struct {
		name     string
		a, b     int
		expected int
	}{
		{"first is larger", 5, 3, 5},
		{"second is larger", 2, 7, 7},
		{"equal values", 4, 4, 4},
		{"negative numbers", -5, -3, -3},
		{"zero and positive", 0, 5, 5},
		{"zero and negative", 0, -3, 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := Max(tt.a, tt.b)
			if result != tt.expected {
				t.Errorf("Max(%d, %d) = %d; want %d",
					tt.a, tt.b, result, tt.expected)
			}
		})
	}
}

t.Run 创建了一个子测试(subtest)。好处是:

  1. 每个子测试有独立的名字,输出更清晰
  2. 一个子测试失败不影响其他子测试
  3. 可以单独运行某个子测试

运行结果:

=== RUN   TestMax
=== RUN   TestMax/first_is_larger
=== RUN   TestMax/second_is_larger
=== RUN   TestMax/equal_values
=== RUN   TestMax/negative_numbers
=== RUN   TestMax/zero_and_positive
=== RUN   TestMax/zero_and_negative
--- PASS: TestMax (0.00s)
    --- PASS: TestMax/first_is_larger (0.00s)
    --- PASS: TestMax/second_is_larger (0.00s)
    ...

单独运行某个子测试:

go test -v -run "TestMax/first_is_larger"

测试辅助函数

当多个测试需要相同的设置逻辑时,可以提取成辅助函数:

func setupTestDB(t *testing.T) *DB {
	t.Helper()  // 标记为辅助函数,错误报告时会指向调用者
	
	db, err := NewDB(":memory:")
	if err != nil {
		t.Fatalf("failed to setup test db: %v", err)
	}
	
	t.Cleanup(func() {
		db.Close()  // 测试结束时清理
	})
	
	return db
}

func TestUserCreation(t *testing.T) {
	db := setupTestDB(t)
	
	user, err := db.CreateUser("张三")
	if err != nil {
		t.Fatalf("CreateUser failed: %v", err)
	}
	
	if user.Name != "张三" {
		t.Errorf("user.Name = %s; want 张三", user.Name)
	}
}

t.Helper() 的作用是让错误报告指向调用辅助函数的地方,而不是辅助函数内部。这在调试时非常有用。

测试并发代码

测试并发代码比较棘手,但 Go 提供了一些工具来帮助你。

func TestConcurrentAccess(t *testing.T) {
	counter := NewSafeCounter()
	
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter.Increment()
		}()
	}
	
	wg.Wait()
	
	if counter.Value() != 100 {
		t.Errorf("counter.Value() = %d; want 100", counter.Value())
	}
}

运行测试时加上 -race 可以检测数据竞态:

go test -race ./...

基准测试(Benchmark)

基准测试用来测量代码的性能。基准测试函数以 Benchmark 开头,参数是 *testing.B:

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Add(100, 200)
	}
}

func BenchmarkMax(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Max(100, 200)
	}
}

运行基准测试:

$ go test -bench=.
BenchmarkAdd-8   	1000000000	         0.2795 ns/op
BenchmarkMax-8   	1000000000	         0.2793 ns/op

-8 表示用了 8 个 CPU 核心。ns/op 是每次操作的纳秒数。

并行基准测试

func BenchmarkMaxParallel(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Max(100, 200)
		}
	})
}

内存分配统计

$ go test -bench=. -benchmem
BenchmarkAdd-8   	1000000000	         0.2795 ns/op	       0 B/op	       0 allocs/op

B/op 是每次操作分配的字节数,allocs/op 是分配次数。这对于优化性能非常有用。

示例测试(Example Tests)

示例测试既是文档,也是测试。函数以 Example 开头:

func ExampleMax() {
	fmt.Println(Max(5, 3))
	fmt.Println(Max(2, 7))
	// Output:
	// 5
	// 7
}

// Output: 注释告诉测试框架预期的输出是什么。如果实际输出不匹配,测试会失败。

示例测试会出现在 go doc 生成的文档中,既是测试又是示例代码。

Mock 和依赖注入

在单元测试中,我们经常需要"假装"某些依赖存在。这就是 Mock 的作用。

通过接口注入依赖

// 定义接口
type UserStore interface {
	GetUser(id int) (*User, error)
}

// 服务依赖接口
type UserService struct {
	store UserStore
}

func NewUserService(store UserStore) *UserService {
	return &UserService{store: store}
}

func (s *UserService) GetUserAge(id int) (int, error) {
	user, err := s.store.GetUser(id)
	if err != nil {
		return 0, err
	}
	return user.Age, nil
}

手动创建 Mock

// Mock 实现
type MockUserStore struct {
	users map[int]*User
}

func (m *MockUserStore) GetUser(id int) (*User, error) {
	user, ok := m.users[id]
	if !ok {
		return nil, errors.New("user not found")
	}
	return user, nil
}

func TestGetUserAge(t *testing.T) {
	store := &MockUserStore{
		users: map[int]*User{
			1: {ID: 1, Name: "张三", Age: 25},
		},
	}

	service := NewUserService(store)

	age, err := service.GetUserAge(1)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if age != 25 {
		t.Errorf("age = %d; want 25", age)
	}

	// 测试不存在的用户
	_, err = service.GetUserAge(999)
	if err == nil {
		t.Error("expected error for non-existent user")
	}
}

测试覆盖率

Go 内置了测试覆盖率工具:

# 查看覆盖率
go test -cover

# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

浏览器会打开一个漂亮的 HTML 报告,显示哪些代码被测试覆盖了,哪些没有。

小贴士:覆盖率是一个有用的指标,但不要盲目追求 100%。有些代码(比如错误处理的边缘情况)很难测试,也不值得测试。关注核心逻辑的覆盖更重要。

测试 HTTP Handler

Go 提供了 httptest 包来方便地测试 HTTP handler:

func TestUsersHandler(t *testing.T) {
	req, err := http.NewRequest("GET", "/users", nil)
	if err != nil {
		t.Fatal(err)
	}

	// 创建一个 ResponseRecorder 来记录响应
	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(usersHandler)

	// 调用 handler
	handler.ServeHTTP(rr, req)

	// 检查状态码
	if status := rr.Code; status != http.StatusOK {
		t.Errorf("status = %d; want %d", status, http.StatusOK)
	}

	// 检查响应体
	var users []User
	if err := json.Unmarshal(rr.Body.Bytes(), &users); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	if len(users) == 0 {
		t.Error("expected non-empty users list")
	}
}

集成测试

对于需要数据库、外部服务等资源的测试,可以用构建标签隔离:

//go:build integration
// +build integration

package mypackage

import "testing"

func TestWithRealDB(t *testing.T) {
	// 连接真实数据库的测试...
}

普通 go test 不会运行它。运行集成测试:

go test -tags=integration

测试最佳实践

1. 一个测试只验证一件事

// 不好:测试做了太多事
func TestUser(t *testing.T) {
	// 创建用户、修改用户、删除用户、登录...
}

// 好:拆分成多个小测试
func TestCreateUser(t *testing.T) { ... }
func TestUpdateUser(t *testing.T) { ... }
func TestDeleteUser(t *testing.T) { ... }

2. 测试命名要清晰

// 不好:看不出测试什么
func Test1(t *testing.T) { ... }

// 好:清晰的意图
func TestCreateUser_WithValidInput_ReturnsUser(t *testing.T) { ... }
func TestCreateUser_WithEmptyName_ReturnsError(t *testing.T) { ... }

3. 遵循 AAA 模式

func TestMax(t *testing.T) {
	// Arrange(准备)
	a, b := 5, 3

	// Act(执行)
	result := Max(a, b)

	// Assert(断言)
	if result != 5 {
		t.Errorf("Max(%d, %d) = %d; want 5", a, b, result)
	}
}

4. 测试错误情况

不要只测试成功路径,也要测试失败路径:

func TestDivide(t *testing.T) {
	// 成功情况
	result, err := Divide(10, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result != 5 {
		t.Errorf("Divide(10, 2) = %d; want 5", result)
	}

	// 失败情况:除以 0
	_, err = Divide(10, 0)
	if err == nil {
		t.Error("expected error when dividing by zero")
	}
}

实战:测试一个完整的包

让我们为一个字符串工具包写完整的测试:

// stringutil/stringutil.go
package stringutil

import (
	"strings"
	"unicode"
)

// Reverse 反转字符串
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)
}

// IsPalindrome 判断是否是回文
func IsPalindrome(s string) bool {
	s = strings.ToLower(s)
	return s == Reverse(s)
}

// WordCount 统计单词数
func WordCount(s string) int {
	return len(strings.Fields(s))
}

// Capitalize 首字母大写
func Capitalize(s string) string {
	if s == "" {
		return s
	}
	runes := []rune(s)
	runes[0] = unicode.ToUpper(runes[0])
	return string(runes)
}
// stringutil/stringutil_test.go
package stringutil

import "testing"

func TestReverse(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{"english word", "hello", "olleh"},
		{"chinese", "你好", "好你"},
		{"empty", "", ""},
		{"single char", "a", "a"},
		{"palindrome", "racecar", "racecar"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := Reverse(tt.input)
			if result != tt.expected {
				t.Errorf("Reverse(%q) = %q; want %q",
					tt.input, result, tt.expected)
			}
		})
	}
}

func TestIsPalindrome(t *testing.T) {
	tests := []struct {
		input    string
		expected bool
	}{
		{"racecar", true},
		{"hello", false},
		{"Racecar", true},  // 不区分大小写
		{"", true},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			result := IsPalindrome(tt.input)
			if result != tt.expected {
				t.Errorf("IsPalindrome(%q) = %v; want %v",
					tt.input, result, tt.expected)
			}
		})
	}
}

func BenchmarkReverse(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Reverse("Hello, World!")
	}
}

func ExampleReverse() {
	fmt.Println(Reverse("Hello"))
	// Output: olleH
}

小结

今天我们全面学习了 Go 的测试技术:

  1. 基础测试:TestXxx 函数、t.Errorf、t.Fatalf
  2. 表驱动测试:用结构体切片组织测试用例
  3. 子测试:t.Run 创建命名子测试
  4. 辅助函数:t.Helper()、t.Cleanup()
  5. 基准测试:BenchmarkXxx、-bench、-benchmem
  6. 示例测试:ExampleXxx,既是文档又是测试
  7. 测试覆盖率:-cover、-coverprofile
  8. HTTP 测试:httptest 包
  9. 最佳实践:AAA 模式、命名规范、错误测试

测试是软件工程的重要组成部分。Go 的测试工具简洁强大,让你能轻松写出可靠的代码。

练习时间

  1. TDD 练习:先写测试,再实现一个 Stack 数据结构
  2. 基准测试:对比 strings.Join 和 + 拼接字符串的性能
  3. 测试 HTTP API:为你之前写的 HTTP 服务器写完整的测试
  4. Mock 练习:用接口和 mock 测试一个依赖外部服务的函数
  5. 覆盖率:为一个包写测试,目标是达到 80% 以上的覆盖率

进阶概念

Testing 框架体系

Go 的测试体系以 testing 包为核心,配合 go test 命令驱动:

  • testing.T:单元测试上下文,提供错误报告、子测试、辅助函数等
  • testing.B:基准测试上下文,管理 b.N 和计时
  • testing.F:模糊测试上下文(Go 1.18+),用于自动发现边界输入
  • testing.M:自定义测试主函数,控制测试前后行为
func TestMain(m *testing.M) {
	// 测试前全局准备
	setup()
	
	code := m.Run()
	
	// 测试后全局清理
	teardown()
	
	os.Exit(code)
}

t.Parallel 并发测试

Go 1.7 引入的 t.Parallel() 让测试函数并发执行,充分利用多核:

func TestSomething(t *testing.T) {
	t.Parallel() // 声明此测试可并发
	
	// 测试逻辑
	result := slowOperation()
	if result != expected {
		t.Errorf("got %v, want %v", result, expected)
	}
}
# 默认串行
go test ./...

# 启用并行(最大 GOMAXPROCS 个并发)
go test -parallel 8 ./...

注意:并行测试之间不能共享可变状态。使用 t.Helper() 配合 sync.Mutex 或独立的 mock 数据来避免竞态。

表驱动子测试

Go 1.7 引入的 t.Run 让表驱动测试更强大:

func TestDivide_Table(t *testing.T) {
	tests := []struct {
		name      string
		a, b      float64
		want      float64
		wantErr   bool
	}{
		{"normal", 10, 2, 5, false},
		{"negative", -10, 2, -5, false},
		{"divide by zero", 10, 0, 0, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// 子测试也可以并行
			t.Parallel()
			
			got, err := Divide(tt.a, tt.b)
			if (err != nil) != tt.wantErr {
				t.Fatalf("Divide() error = %v, wantErr %v", err, tt.wantErr)
			}
			if !tt.wantErr && got != tt.want {
				t.Errorf("Divide() = %v, want %v", got, tt.want)
			}
		})
	}
}

b.Cleanup

testing.B 也支持 Cleanup,确保基准测试前后环境干净:

func BenchmarkDBQuery(b *testing.B) {
	db, err := setupTestDB()
	if err != nil {
		b.Fatal(err)
	}
	b.Cleanup(func() { db.Close() })

	b.ResetTimer() // 重置计时器,不计入 setup 时间
	for i := 0; i < b.N; i++ {
		db.Query("SELECT * FROM users WHERE id = ?", i)
	}
}

模糊测试 Fuzzing(Go 1.18+)

模糊测试自动生成随机输入,帮助发现边界情况下的 panic 或逻辑错误:

func FuzzReverse(f *testing.F) {
	// 种子语料
	f.Add("hello")
	f.Add("世界")
	f.Add("")

	f.Fuzz(func(t *testing.T, orig string) {
		rev := Reverse(orig)
		doubleRev := Reverse(rev)
		if orig != doubleRev {
			t.Fatalf("before: %q, after: %q", orig, doubleRev)
		}
		if utf8.ValidString(orig) && !utf8.ValidString(rev) {
			t.Fatalf("Reverse produced invalid UTF-8 string %q", rev)
		}
	})
}
# 运行模糊测试(默认 1 秒)
go test -fuzz=FuzzReverse

# 指定更长的模糊测试时间
go test -fuzz=FuzzReverse -fuzztime=30s

模糊测试非常适合字符串处理、数据解析等容易在边界输入出错的场景。

实战案例

HTTP 服务集成测试

package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestCreateUser_Integration(t *testing.T) {
	// 创建测试服务器
	mux := http.NewServeMux()
	mux.HandleFunc("/users", createUserHandler)
	server := httptest.NewServer(mux)
	defer server.Close()

	// 准备请求
	body, _ := json.Marshal(map[string]string{
		"name":  "张三",
		"email": "zhangsan@example.com",
	})

	resp, err := http.Post(
		server.URL+"/users",
		"application/json",
		bytes.NewReader(body),
	)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		t.Errorf("status = %d; want %d", resp.StatusCode, http.StatusCreated)
	}

	// 验证响应体
	var result map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&result)
	if result["name"] != "张三" {
		t.Errorf("name = %v; want 张三", result["name"])
	}
}

Mock 测试

package main

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

// 被测接口
type PaymentGateway interface {
	Charge(ctx context.Context, amount int64, token string) (string, error)
	Refund(ctx context.Context, chargeID string, amount int64) error
}

// Mock 实现
type MockGateway struct {
	mock.Mock
}

func (m *MockGateway) Charge(ctx context.Context, amount int64, token string) (string, error) {
	args := m.Called(ctx, amount, token)
	return args.String(0), args.Error(1)
}

func (m *MockGateway) Refund(ctx context.Context, chargeID string, amount int64) error {
	args := m.Called(ctx, chargeID, amount)
	return args.Error(0)
}

type OrderService struct {
	gateway PaymentGateway
}

func (s *OrderService) CreateOrder(ctx context.Context, amount int64, token string) error {
	chargeID, err := s.gateway.Charge(ctx, amount, token)
	if err != nil {
		return err
	}
	// 保存订单...
	_ = chargeID
	return nil
}

func TestOrderService_CreateOrder(t *testing.T) {
	mockGW := new(MockGateway)
	mockGW.On("Charge", mock.Anything, int64(10000), "tok_visa").
		Return("ch_123", nil)

	service := &OrderService{gateway: mockGW}
	err := service.CreateOrder(context.Background(), 10000, "tok_visa")

	assert.NoError(t, err)
	mockGW.AssertExpectations(t)
}

注意:上面的示例使用了 stretchr/testify/mock。如果不想引入依赖,可以手写 mock,核心是用接口隔离依赖。

数据库事务回滚测试

package main

import (
	"database/sql"
	"testing"

	_ "github.com/mattn/go-sqlite3"
)

func setupTestDB(t *testing.T) *sql.DB {
	db, err := sql.Open("sqlite3", ":memory:")
	if err != nil {
		t.Fatal(err)
	}
	
	// 创建测试表
	db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`)
	
	t.Cleanup(func() {
		db.Close()
	})
	
	return db
}

func TestUserRepository_Create(t *testing.T) {
	db := setupTestDB(t)
	repo := NewUserRepository(db)
	
	// SQLite 内存模式下不加显式事务就是自动提交
	// 如果需要事务回滚测试,可以显式控制
	tx, _ := db.Begin()
	repoTx := NewUserRepository(tx)
	
	user, err := repoTx.Create("张三")
	if err != nil {
		t.Fatalf("create failed: %v", err)
	}
	if user.Name != "张三" {
		t.Errorf("name = %s; want 张三", user.Name)
	}
	
	// 回滚事务,测试隔离
	tx.Rollback()
	
	// 验证主 db 中确实没有这条记录
	var count int
	db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
	if count != 0 {
		t.Errorf("expected 0 users after rollback, got %d", count)
	}
}

测试策略对比表

维度单元测试集成测试E2E 测试
覆盖范围单个函数/方法多个模块协作完整用户流程
依赖Mock 外部依赖真实依赖(DB、HTTP)完整系统部署
执行速度毫秒级秒级分钟级
稳定性高中(受环境波动)低(全链路依赖)
维护成本低中高
定位问题精确较精确模糊
成本收益比高中低
推荐比例70%20%10%

FAQ

Q1: 什么是测试中的 AAA 模式?

A: AAA 代表 Arrange-Act-Assert(准备-执行-断言)。这是一种组织测试代码的标准模式:先设置输入和依赖(Arrange),然后调用被测函数(Act),最后验证输出(Assert)。遵循 AAA 模式能让测试更易读和维护。

Q2: t.Helper() 有什么用,什么时候用?

A: t.Helper() 标记当前函数为辅助函数。当测试失败时,错误信息会指向调用辅助函数的代码行,而不是辅助函数内部。这能帮助你更快定位问题。任何提取出来的测试工具函数都应该调用 t.Helper()。

Q3: 我应该用什么断言库?

A: Go 标准库测试不用断言库,直接用 if + t.Errorf。但第三方库 testify/assert 更便利:assert.Equal(t, expected, actual)。选择标准库的 if 风格可以获得更好错误信息控制,选择 testify 可以写更少的样板代码。两者都是合理选择。

Q4: 如何运行项目中的所有测试?

A: 使用递归模式:

go test ./...              # 运行目录下所有测试
go test -v ./...           # 显示详细输出
go test -race ./...        # 启用竞态检测
go test -cover ./...       # 显示覆盖率
go test -count=1 ./...     # 禁用测试缓存强制重跑

Q5: 基准测试中 b.N 是怎么变化的?

A: Go 测试框架会逐步增大 b.N(1, 100, 10000…),直到基准测试运行时间足够稳定。你的代码不需要关心 N 的值,只需要在循环中执行待测操作即可。最终报告的结果是基于最佳稳定采样。

Q6: 模糊测试和传统单元测试的区别是什么?

A: 传统单元测试用你手动指定的输入验证行为。模糊测试自动生成海量随机输入,寻找能让程序 panic 或行为异常的边界值。两者互补:单元测试覆盖已知重要场景,模糊测试发现未知漏洞。

Q7: 如何处理测试中时间相关的逻辑?

A: 三种策略:1)通过接口注入时钟依赖(如 clock.Clock),测试中替换为可以控制时间的 mock;2)用 time.Now() 的返回值做范围判断而非精确比较;3)对时间敏感的逻辑提取为纯函数,将时间作为参数传入。

Q8: 测试覆盖率 100% 就一定没问题吗?

A: 不是。覆盖率只衡量代码被执行,不衡量执行路径的验证完整性。一条路径可能执行了但断言没有检查所有结果。覆盖率是必要指标但非充分条件——核心目标是测试行为而非覆盖代码行。

最佳实践

测试命名规范

遵循项目统一的命名约定,通常采用 Test函数名_场景_期望结果 的格式:

func TestProcessPayment_ValidCard_ChargesSuccessfully(t *testing.T) { }
func TestProcessPayment_InvalidCard_ReturnsError(t *testing.T) { }
func TestProcessPayment_NetworkError_RetriesAndSucceeds(t *testing.T) { }

这种命名一眼就能看出测试场景和期望,在测试失败时也有很大帮助。

断言库选择建议

  • 标准库:适合纯 Go 项目和希望零依赖的场景
  • testify/assert:适合团队使用、代码量大的项目,减少样板代码
  • gomega/gomega:如果习惯 BDD 风格的测试描述

无论选择哪个,保持项目内部一致很重要。

延伸阅读

我们下篇见!


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练