go单元测试以表驱动为标准实践,需用*testing.t参数、test开头命名,通过t.run实现子测试隔离与并行,依赖抽象接口+mock或httptest避免真实调用。

Go 的单元测试不需要额外框架,go test 原生支持,但写得清晰、可维护、易扩展的关键在于结构设计——表驱动测试不是“高级技巧”,而是 Go 测试的默认实践方式。
如何用 testing.T 写基础单元测试
每个测试函数必须接收 *testing.T 参数,且函数名以 Test 开头(如 TestAdd)。Go 不支持断言宏,所有判断都靠 t.Errorf 或 t.Fatal 显式触发失败。
常见错误是直接 panic 或忽略错误返回值:比如调用一个返回 error 的函数后没检查,测试就“看似通过”了。正确做法是把 error 检查作为测试逻辑一部分。
-
func TestAdd(t *testing.T)必须在xxx_test.go文件中,且与被测代码同包(除非测导出接口) - 不要在测试里用
log.Fatal或os.Exit,它们会跳过testing的清理流程 - 测试函数内避免全局状态修改(如改包级变量),否则多个测试间可能互相污染
为什么表驱动测试是 Go 的标准写法
因为 Go 没有参数化测试语法(如 pytest 的 @pytest.mark.parametrize),而手动写一堆 TestXxx1、TestXxx2 会导致重复代码和维护困难。表驱动把输入、预期、说明聚合成结构体切片,用一个循环跑完全部用例。
它不只是“少写几行”,更关键的是让新增用例变成追加一行结构体,而不是复制粘贴整个函数。同时便于覆盖边界值(空字符串、负数、nil)、错误路径(如 I/O 失败模拟)和性能敏感场景(大输入)。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 典型结构:
tests := []struct{ input int; want int; name string }{...} - 每个子测试用
t.Run(name, func(t *testing.T){...})隔离,失败时能精准定位到name - 别把所有字段塞进 struct:只放真正变化的输入和预期;常量(如超时时间)提成局部变量
t.Run 的作用远不止分组命名
t.Run 创建子测试,不仅让 go test -run=TestParse/invalid_json 这类模糊匹配生效,更重要的是它提供独立的生命周期:每个子测试有自己的 t.Cleanup、可单独设置 t.Parallel()、且 t.Fatal 只终止当前子测试而非整个 TestXxx 函数。
容易踩的坑是忘记加 t.Run 直接循环,导致所有用例共享同一个 t 实例——一旦某个用例 t.Fatal,后续用例全跳过,且 -run 过滤失效。
- 必须在循环体内调用
t.Run,不能在外层定义函数再传入(闭包捕获变量会出错) - 如果用例间无依赖,加
t.Parallel()能显著缩短总测试时间,但注意:并发测试不能共享可变状态(如文件、数据库连接、全局 map) -
t.Cleanup在子测试结束时执行,适合关闭临时文件、恢复环境变量等,比 defer 更可靠(defer 在函数返回时才触发,而子测试可能被t.Fatal中断)
如何处理依赖外部服务的测试(如 HTTP、DB)
真实项目里,单元测试不该发起真实网络请求或连数据库。正确做法是抽象接口(如 type Client interface { Do(req *http.Request) (*http.Response, error) }),然后在测试中传入 mock 实现。
别用 monkey.Patch 这类运行时打桩工具——它破坏类型安全、难调试、且不兼容 go:generate 和某些构建模式。Go 推荐的是“依赖注入 + 接口隔离”。
- 被测函数接收依赖作为参数(如
func Process(ctx context.Context, client Client) error),而非内部直接调用http.DefaultClient.Do - 测试时构造一个满足接口的匿名结构体:
client := &mockClient{resp: &http.Response{...}} - 对简单场景,用
httptest.Server启一个本地 HTTP 服务,比 mock 更接近真实行为,且无需额外依赖
表驱动测试的复杂点不在语法,而在用例设计:哪些输入必须覆盖、错误路径是否显式声明、子测试命名是否足够描述行为。很多人卡在“不知道该测什么”,而不是“不会写 t.Run”。先列出函数所有可能的输入分类(正常值、边界值、错误值),再为每类选 1–2 个代表,比堆砌 20 个相似用例有用得多。










