Go 高手(14):测试进阶——mock、httptest、testify
更新时间:2026-09-01。本文是
languages/go/intermediate/高手层第 14 篇,接 database/sql。入门层讲了表驱动测试,进阶层讲进阶技巧:mock 依赖、httptest 测试 HTTP、testify 简化断言,以及表驱动测试的高级玩法。
本文要回答的问题
- 单元测试为什么需要 mock?怎么 mock 依赖?
- 如何测试 HTTP handler 不用真启动服务?
- testify/assert 的作用是什么?能减少什么代码?
- table-driven 测试怎么处理复杂参数和预期结果?
一、mock 依赖:隔离被测试单元
单元测试的目标是测试一个函数/模块,不需要真正调用数据库、HTTP 接口等外部依赖。把依赖 mock 掉,测试才能快。
Go 里 mock 依赖靠接口:
// 定义接口,让依赖满足它
type UserGetter interface {
GetUser(id int) (*User, error)
}
// 业务逻辑依赖接口,不依赖具体实现
func service(getter UserGetter, id int) (*User, error) {
return getter.GetUser(id)
}测试时传入 mock 实现:
type MockGetter struct {
mock.Mock
}
func (m *MockGetter) GetUser(id int) (*User, error) {
ret := m.Called(id)
return ret.Get(0).(*User), ret.Error(1)
}
func TestService(t *testing.T) {
mockGetter := new(MockGetter)
mockGetter.On("GetUser", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
u, err := service(mockGetter, 1)
assert.NoError(t, err)
assert.Equal(t, "Alice", u.Name)
mockGetter.AssertExpectations(t)
}常用 mock 工具:
- github.com/stretchr/testify/mock
- github.com/golang/mock(代码生成)
- 简单场景手动写 mock 更清晰
二、httptest:测试 HTTP handler
不用真启动服务器监听端口,用 httptest 直接测试 handler:
func TestMyHandler(t *testing.T) {
// 创建假 request 和 response
req, _ := http.NewRequest("GET", "/foo", nil)
w := httptest.NewRecorder()
// 直接调用 handler
handler := myHandler()
handler.ServeHTTP(w, req)
// 断言结果
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "OK")
}w := httptest.NewRecorder() 实现了 http.ResponseWriter,结果存在内存里,可以直接读状态码、头、body。
测试带 context 的 handler:
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler(w, req)
// 一样用三、testify/assert:简化断言
不用写一堆 if got != want { t.Errorf(...) },用 assert.Equal 一句话搞定:
import "github.com/stretchr/testify/assert"
// 原生写法
if got != want {
t.Errorf("got %d, want %d", got, want)
}
// testify 写法
assert.Equal(t, want, got)
assert.True(t, ok)
assert.Contains(t, body, "expected")
assert.Error(t, err)
assert.NoError(t, err)常见 assert 函数:
assert.Equal(t, want, got)→ 相等assert.NotEqual(t, want, got)→ 不相等assert.Contains(t, string, substring)→ 包含子串assert.Contains(t, slice, element)→ 切片包含元素assert.True(t, condition)→ condition 是 trueassert.Error(t, err)→ err != nilassert.NoError(t, err)→ err == nil
四、table-driven 测试进阶
复杂案例可以用结构体描述输入输出,包括 error:
func TestDivide(t *testing.T) {
cases := []struct {
name string
a, b int
want int
wantErr bool
}{
{"positive", 6, 2, 3, false},
{"zero", 6, 0, 0, true},
{"negative", -6, 2, -3, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, err := Divide(c.a, c.b)
if (err != nil) != c.wantErr {
t.Errorf("Divide error expected %v, got %v", c.wantErr, err != nil)
return
}
if !c.wantErr && got != c.want {
t.Errorf("Divide got %d, want %d", got, c.want)
}
})
}
}用 testify 更简洁:
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, err := Divide(c.a, c.b)
if c.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, c.want, got)
}
})
}五、测试覆盖文件系统
用 embed.FS 或 os.File 可以在测试时加载测试数据,testing/fstest 提供专门的测试工具:
import "testing/fstest"
fs := fstest.MapFS{
"hello.txt": {Data: []byte("hello world")},
"dir/foo.txt": {Data: []byte("foo")},
}fstest.MapFS 在内存里模拟文件系统,不用写临时文件,测试更干净。
六、测试数据库:in-memory 数据库
测试数据库操作时,用 in-memory 数据库比如 sqlite3:file:test.db?mode=memory&cache=shared:
db, err := sql.Open("sqlite3", "file:test.db?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
defer db.Close()
// 直接测试 migrate 和查询不用启动真实数据库服务,测试快,不用清理。
七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 不 mock 依赖,测试依赖网络/数据库 | 测试慢、不稳定 | 用接口 + mock,隔离依赖 |
| 测试不隔离,全局状态共享 | 测试互相干扰 | 每次测试创建新对象,不用全局 |
| httptest 后不检查状态码 | 测试不完整 | 先 assert 状态码,再 assert 内容 |
| mock 后不验证调用次数 | mock 没被调用也通过 | 用 assertExpectations 验证调用 |
相关与延伸
下一篇:Go 工具链——go mod、go build、go get、go vet;入门层单元测试基础见 单元测试。
一句话总结
Go 测试进阶:单元测试用接口 mock 依赖,隔离外部,测试变快变稳定;用 httptest 直接测试 HTTP handler,不用启动服务;testify/assert 简化断言,少写很多 t.Errorf;复杂单元测试用表驱动,每个案例子测试;数据库测试用 in-memory 数据库,不用启动真实服务。