最轻量可控的方式是直接替换全局变量 exec.command 为自定义函数,需在包顶层声明 var command = exec.command,测试中用 defer 恢复原值,并确保 fakecommand 返回的 *exec.cmd 正确设置 stdout/stderr/processstate。

Go 测试里怎么替换 exec.Command 的实际调用
直接替换函数指针最轻量,也最可控。Go 没有运行时 mock 框架,靠的是把 exec.Command 这个变量本身设成你自己的函数——它本就是个可导出的全局变量。
常见错误是试图在测试里用 monkey.Patch 或反射去改,反而引入依赖、破坏并发安全,或者 patch 失败却没报错,导致测试跑的还是真实命令。
- 在包顶层声明:
var Command = exec.Command(注意别漏掉var,否则是局部变量) - 测试前重赋值:
old := Command; Command = fakeCommand; defer func() { Command = old }() - 务必用
defer恢复,否则一个测试污染会影响后续所有测试(尤其go test -race下更容易暴露)
写 fakeCommand 时要注意哪些返回值细节
exec.Command 返回的是 *exec.Cmd,而它的 Run / Output / CombinedOutput 行为必须和真实命令对齐,否则调用方 panic 或逻辑跳错。
典型坑:只 mock Run() 却没处理 Output(),结果被测代码调用 cmd.Output() 时 panic “exec: not started”。
- 返回的
*exec.Cmd必须设置Cmd.Stdout/Cmd.Stderr/Cmd.Stdin(哪怕设成nil或io.Discard) - 实现
Run()时,要手动调用cmd.ProcessState = &os.ProcessState{...},否则err == nil但cmd.ProcessState.Success()报 panic - 更稳妥的做法:用
exec.Command("true")或exec.Command("false")做底座,只替换其StdoutPipe等方法,避免自己拼ProcessState
为什么不能只 mock Cmd.Run 而要整个替换 exec.Command
因为被测代码可能在任意位置调用 exec.Command("ls", "-l"),也可能先存下函数引用再调用:cmd := exec.Command; cmd("sh", "-c", "date")。只 patch 方法无法覆盖后者。
另一个现实问题:有些库(比如 golang.org/x/sys/unix 相关封装)会绕过 exec.Command 直接调用系统调用,但那是另一层问题——你 mock 的目标只是 Go 标准库路径上的命令启动行为。
- 只 mock
Cmd.Run对exec.CommandContext无效(它内部仍调用原始exec.Command) - 如果被测函数接收
*exec.Cmd作为参数,那另当别论;但绝大多数情况,源头都在exec.Command调用点 - 替换全局
Command变量是 Go 官方文档明确推荐的方式(见exec包文档 Example 部分)
真实场景下怎么模拟不同命令的返回差异
不是所有测试都只需要“成功”或“失败”,比如测试错误解析逻辑时,需要特定退出码 + 特定 stderr 内容。
硬编码一个 fake 函数应付不了多分支,容易让测试逻辑和 mock 逻辑混在一起,难维护。
- 用闭包构造带状态的 fake:
fakeCommand := func(name string, args ...string) *exec.Cmd { ... },内部用switch name或 map 查表 - 把预期输入输出写成结构体切片,在测试 setup 阶段注册,fake 函数按顺序返回(适合验证调用顺序)
- 避免在 fake 里做复杂断言——mock 只负责返回,断言留在测试主体里,职责清晰
最难搞的其实是那些没显式调用 exec.Command、而是通过第三方库间接触发的命令,比如某些 config loader 会自动执行 git rev-parse。这时候得看库是否提供注入点,而不是强行 mock 底层 syscall。










