interface{}不是万能测试替身,因绕过类型检查导致运行时panic;应定义最小聚焦接口、用字段控制mock行为、注意指针接收者匹配、避免循环复用mock实例。

为什么 interface{} 不是万能的测试替身
Go 测试里盲目用空接口替代依赖,反而会让测试更脆弱。它绕过了类型约束,导致编译期无法发现方法签名不一致、漏实现等问题,等跑测试时才报 panic: interface conversion: *mock.X is not Y: missing method Z。
真正该做的是定义最小、聚焦的接口,只包含被测代码实际调用的方法:
- 比如 HTTP handler 只调用了
repo.FindByID()和cache.Set(),就别塞进Update()或Delete() - 接口名要体现角色,如
UserReader比UserRepo更准确——测试时你只读,不关心它底层是 SQL 还是内存 map - 把接口定义放在被测包里(或 shared 包),而非 mock 包中,避免“为 mock 而抽象”
如何写一个不泄漏真实实现细节的 mock
用结构体字段控制行为,而不是在方法里硬编码逻辑。这样测试可读、可组合、易复位。
常见错误是写成这样:
立即学习“go语言免费学习笔记(深入)”;
func (m *MockDB) Get(id int) (*User, error) {
if id == 1 { return &User{Name: "test"}, nil }
return nil, errors.New("not found")
}
这会让多个测试用例互相污染。正确做法是:
- 用字段存返回值:
GetReturn *User和GetError error - 在测试 setup 阶段显式设置:
mock.GetReturn = &User{Name: "alice"} - 方法体保持统一:
return m.GetReturn, m.GetError - 如果需要多次调用不同响应,用切片 + 索引计数器,而不是 if-else 分支
测试中传入 interface 却 panic:检查是否漏了指针接收者
这是最常被忽略的 Go 类型系统细节。当你定义:
func (u User) Name() string { return u.name }
那么 User{} 实现了该方法,但 *User 也实现了——而反过来不成立。如果你的 mock 是指针类型 *MockUser,但接口方法是值接收者,那没问题;可一旦接口方法是指针接收者:
func (u *User) Save() error { ... }
你就必须传 *MockUser,否则类型断言失败,运行时报 panic: interface conversion: MockUser is not Saver: missing method Save。
- 快速自查:看接口方法的接收者是不是
*T,对应 mock 的类型是不是*MockT - 生成 mock 时(比如 gomock),确保
-destination输出的是指针类型结构体 - 不要靠 “反正都试试” 硬试,Go 的接口满足是静态检查的,IDE 或
go build就能提前报错
表格驱动测试 + 接口 mock 容易卡在哪
表格驱动本身没问题,但和接口 mock 组合时,容易在循环里复用同一个 mock 实例,导致状态串扰。
例如:
tests := []struct{
name string
input int
wantErr bool
}{...}
mock := &MockRepo{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 这里没重置 mock 字段!
got, err := doSomething(mock, tt.input)
...
})
}
第二次迭代可能读到第一次写入的 mock.SaveCalled 计数,结果断言失败。
- 每个子测试里 new 一个新的 mock:
mock := &MockRepo{} - 或者把 mock 初始化放进 test case 结构体里,作为字段一并初始化
- 如果 mock 有内部状态(如调用次数、参数记录),务必在每次子测试开始前清零
接口抽象本身不难,难的是每次写 mock 时都得问一句:这个方法我真需要它返回什么?还是只是想确认它被调用了?后者用函数变量更轻量,别一上来就建 struct。










