Go语言mock成败取决于代码写法:接口须由使用方定义最小契约,依赖必须通过构造函数显式注入,I/O操作(如HTTP、DB)需作为参数传入而非硬编码。

Go 语言里 mock 不是加个工具就能用,而是代码写法决定你能不能 mock——接口定义位置、依赖传入方式、I/O 是否硬编码,这三件事没做对,gomock 或 testify/mock 生成再多代码也救不回来。
接口必须由使用方定义,不能复用第三方包的类型
常见错误是直接 import 第三方 SDK 包(比如 github.com/aws/aws-sdk-go-v2/service/s3),然后把它的 s3.Client 当作参数传进业务函数。编译会报错:cannot use *mockS3Client as *s3.Client,因为 Go 不允许指针类型互换。
- 正确做法:在你的业务包(如
order/internal/dep)里定义最小接口,只包含你真正用到的方法,比如GetObject(ctx, params)和PutObject(ctx, params) - 真实 S3 客户端要包装一层适配器,让它实现这个接口;测试时用
struct{}或手写 mock 实现同一接口即可 - 避免把接口放在第三方包里——否则测试就得 import 生产依赖,破坏隔离
构造函数必须显式接收依赖,禁用全局变量和 init 初始化
像 var db *sql.DB = sql.Open(...) 这种写法会让所有测试共享同一个连接,且无法替换成 sqlmock 实例,TestA 改了状态可能让 TestB 失败。
- 业务结构体的构造函数(如
NewOrderService)必须把所有外部依赖作为参数接收,类型是接口而非具体实现 - 不要在包级作用域初始化任何资源;
init()函数里调用log.Fatal或os.Exit会让测试直接退出,改用返回error - main 函数负责组装真实依赖,测试文件负责传入 mock,职责彻底分离
HTTP、数据库、Stdin/Stdout 等 I/O 必须作为参数传入
函数里出现 http.Get、fmt.Println、bufio.NewReader(os.Stdin),基本等于放弃单元测试。重定向 stdin/stdout 是补救,不是设计。
立即学习“go语言免费学习笔记(深入)”;
- 把
http.Client、io.Reader、io.Writer全部设为函数参数或结构体字段 - 例如
ProcessInput(reader io.Reader, writer io.Writer) error,测试时用strings.NewReader("yes")和bytes.NewBuffer(nil)即可断言输出 - 别在函数内部 new
http.Client——它带默认 Transport,没法 mock 超时或失败场景;传入 client 才能控制底层 RoundTripper
Mock 不是越多越好,优先手写轻量 mock
接口只有 1–2 个方法时,用 gomock 生成一堆代码反而增加维护成本。手写 mock 更快、更可控,也更容易做表驱动测试。
- 比如
UserRepo只有GetByID(id int) (*User, error),直接写type MockUserRepo struct { users map[int]*User }就够了 - 需要动态返回值或校验调用次数时,再考虑
gomock;日常测试中,多数场景只需返回固定值或 error - mock 结构体字段尽量公开(如
ErrOnGet bool),方便测试用例灵活配置,而不是每次都要重写方法
最常被忽略的一点:mock 的生命周期必须和被测对象一致。传进去的 mock 实例如果被缓存、被多个 goroutine 共享、或在测试中途被修改,结果就不可预测——这不是框架问题,是依赖传递路径没理清。


















