如何使用Golang编写单元测试_Golang testing包单元测试示例

Go单元测试需满足文件名以_test.go结尾、函数名以Test开头、参数为*testing.T;go test默认只运行当前目录测试,-run支持正则匹配;应避免log.Fatal/os.Exit,改用t.Fatal/t.Error;推荐表驱动测试与接口抽象解耦外部依赖。

如何用 go test 运行最简单元测试

Go 的单元测试不需要额外框架,只要文件名以 _test.go 结尾、函数名以 Test 开头、参数为 *testing.T,就能被 go test 自动识别。不满足任一条件,测试就不会执行。

  • go test 默认只运行当前目录下的 *_test.go 文件,不会递归子目录
  • 测试函数必须是导出的(首字母大写),但名字本身不用导出 —— TestAdd 合法,testAdd 会被忽略
  • 若想只跑某个测试,用 go test -run=TestAdd-run 后跟的是正则匹配名,不是函数全名

testing.T 的常见误用:别在测试里用 log.Fatalos.Exit

测试中调用 log.Fatalpanicos.Exit(1) 会导致整个测试进程退出,后续测试全部中断,且 go test 会报 exit status 1,掩盖真实失败原因。

  • 正确做法是调用 t.Fatalt.Errorf + t.FailNow(),它们只终止当前测试函数,不影响其他测试
  • t.Error 记录错误但继续执行,适合检查多个断言;t.Fatal 遇错即停,适合前置条件校验(如初始化失败)
  • 如果被测函数内部调用了 os.Exit(比如 CLI 工具),需通过接口抽象或构建可替换的 os.Exit 函数变量来解耦,否则无法测试

表驱动测试怎么写才不重复又易维护

Go 社区推荐用结构体切片定义测试用例,避免大量复制粘贴的 TestXxx 函数。关键在于把输入、期望输出、描述聚合成一个数据项,再用循环统一执行断言。

func TestParseDuration(t *testing.T) {
    cases := []struct {
        name     string
        input    string
        expected time.Duration
        wantErr  bool
    }{
        {"zero", "0s", 0, false},
        {"seconds", "30s", 30 * time.Second, false},
        {"invalid", "1y", 0, true},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            got, err := time.ParseDuration(tc.input)
            if tc.wantErr {
                if err == nil {
                    t.Fatal("expected error, got nil")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if got != tc.expected {
                t.Errorf("ParseDuration(%q) = %v, want %v", tc.input, got, tc.expected)
            }
        })
    }
}
  • t.Run 创建子测试,让每个用例独立计时、独立失败,输出带名字(如 --- FAIL: TestParseDuration/seconds
  • 结构体字段名要语义清晰,name 必须有,否则子测试名是空字符串,调试困难
  • 不要在循环里直接用 tc 变量做闭包 —— Go 中循环变量复用,所有 goroutine 或延迟函数会看到最后一个值;必须用 tc := tc 显式拷贝

测试依赖外部服务?用接口+模拟(mock)而不是真实调用

测试不应该依赖数据库、HTTP 服务或文件系统。真实 I/O 不仅慢、不稳定,还会让测试变成集成测试。Go 推崇“依赖倒置”:把具体实现抽成接口,测试时传入内存实现。

  • 例如 HTTP 客户端,不要直接用 http.DefaultClient,而是定义 type HTTPDoer interface { Do(*http.Request) (*http.Response, error) }
  • 测试时传入自定义类型,Do 方法返回预设响应,完全绕过网络
  • 对标准库类型(如 *sql.DB)也一样:定义 Querier 接口,只暴露 QueryRow 等方法,测试时用内存 map 模拟结果
  • 避免使用第三方 mock 库(如 gomock),多数场景纯 Go 就够用;mock 库增加编译依赖和学习成本,且容易过度模拟

测试最难的不是写断言,而是识别哪些逻辑该被隔离、哪些状态需要重置。比如全局变量、单例、时间相关代码(time.Now())、随机数生成器(rand.Intn)——这些都得通过参数注入或接口抽象才能可控。没做这一步,测试就只是“能跑”,不是“可信”。