go单元测试要求解耦依赖,因硬编码依赖导致测试变慢、非确定性和暴露敏感配置;需将依赖改为参数或字段,定义最小接口并用mock struct实现,无需第三方库。

不能直接测,必须先解耦依赖——Go 的单元测试不接受“硬编码依赖”。
为什么 TestAdd 不能套用到业务函数上
像 Add 这类纯函数,输入确定、无副作用,开箱即测。但真实业务函数往往调用 http.Client、sql.DB、time.Now() 或第三方 SDK,这些行为会:
- 让测试变慢(网络超时、DB 连接建立)
- 导致非确定性(时间漂移、DB 状态污染)
- 暴露敏感配置(测试里写进 DSN 或 API key)
- 违反单元测试“只测当前逻辑”的边界
所以不是测试写得不对,是函数本身没设计成可测试的。
重构函数:把依赖变成参数或字段
核心动作就一条:把“自己 new 出来的依赖”改成“别人传进来的接口”。例如:
坏写法(不可测):
func ProcessOrder(id string) error {
db, _ := sql.Open("mysql", os.Getenv("DSN"))
_, err := db.Exec("UPDATE orders SET status=? WHERE id=?", "processed", id)
return err
}
好写法(可测):
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
type DB interface {
Exec(query string, args ...interface{}) (sql.Result, error)
}
func ProcessOrder(db DB, id string) error {
_, err := db.Exec("UPDATE orders SET status=? WHERE id=?", "processed", id)
return err
}
关键点:
- 定义最小接口,只包含该函数实际用到的方法
- 函数签名显式接收依赖,而不是内部创建
- 生产代码中用
sql.DB实例传入(它天然满足DB接口) - 测试时传一个内存 struct 就行,不用启 DB
测试时怎么 mock 那个 DB 接口
不需要第三方库。Go 原生支持隐式接口实现,mock 就是写个 struct:
type mockDB struct {
execFunc func(query string, args ...interface{}) (sql.Result, error)
}
func (m mockDB) Exec(query string, args ...interface{}) (sql.Result, error) {
return m.execFunc(query, args...)
}
func TestProcessOrder(t *testing.T) {
tests := []struct {
name string
db DB
id string
wantErr bool
}{
{
name: "updates row successfully",
db: mockDB{execFunc: func(_ string, _ ...interface{}) (sql.Result, error) {
return &fakeResult{}, nil
}},
id: "123",
wantErr: false,
},
{
name: "returns error on exec failure",
db: mockDB{execFunc: func(_ string, _ ...interface{}) (sql.Result, error) {
return nil, errors.New("timeout")
}},
id: "123",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ProcessOrder(tt.db, tt.id)
if (err != nil) != tt.wantErr {
t.Errorf("ProcessOrder() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
注意:
- mock 结构体只在
_test.go文件里定义,不导出,不进构建 - 用闭包控制行为,比全局变量更安全、更易复位
- 别用
gomonkey或gock去 patchsql.Open——那是在修补丁,不是写单元测试
时间、HTTP、文件等依赖同理处理
所有“有副作用”的东西都按同样思路抽象:
- 时间:把
time.Now()换成func() time.Time字段或参数 - HTTP:定义
HTTPDoer接口,包含Do(*http.Request) (*http.Response, error),测试时用httptest.Server或返回固定响应的 mock - 文件:定义
FileReader接口,方法为ReadFile(name string) ([]byte, error),测试时返回预设字节或错误
真正难的从来不是怎么 mock,而是要不要为某个函数专门抽一个接口——答案很务实:只在测试需要隔离它的时候才抽,且接口越小越好。一个函数只用到 Write,就别定义带 Read 和 Seek 的大接口。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










