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)。好处是:
- 每个子测试有独立的名字,输出更清晰
- 一个子测试失败不影响其他子测试
- 可以单独运行某个子测试
运行结果:
=== 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 的测试技术:
- 基础测试:
TestXxx函数、t.Errorf、t.Fatalf - 表驱动测试:用结构体切片组织测试用例
- 子测试:
t.Run创建命名子测试 - 辅助函数:
t.Helper()、t.Cleanup() - 基准测试:
BenchmarkXxx、-bench、-benchmem - 示例测试:
ExampleXxx,既是文档又是测试 - 测试覆盖率:
-cover、-coverprofile - HTTP 测试:
httptest包 - 最佳实践:AAA 模式、命名规范、错误测试
测试是软件工程的重要组成部分。Go 的测试工具简洁强大,让你能轻松写出可靠的代码。
练习时间
- TDD 练习:先写测试,再实现一个
Stack数据结构 - 基准测试:对比
strings.Join和+拼接字符串的性能 - 测试 HTTP API:为你之前写的 HTTP 服务器写完整的测试
- Mock 练习:用接口和 mock 测试一个依赖外部服务的函数
- 覆盖率:为一个包写测试,目标是达到 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 风格的测试描述
无论选择哪个,保持项目内部一致很重要。
延伸阅读
- Go 语言文档 - testing 包
- Go 官方博客 - Test-driven development with Go
- Go Wiki - TableDrivenTests
- Go 模糊测试官方教程
- Testify - 流行的 Go 测试辅助库
我们下篇见!
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。