编写第一个单元测试:从零到PASS

好,咱们直接动手。这一章的目标很简单——写一个真正的单元测试,跑起来,看懂输出。我见过太多人把测试想得太玄乎,其实说白了,测试就是一段代码,用来验证另一段代码的行为对不对。

被测函数设计:先有被测试的代码

写测试之前,总得有个被测对象吧?我个人习惯先写一个简单的工具函数,这样能聚焦在测试本身,不会被业务逻辑干扰。

假设我们要写一个加法函数:

// math.go
package main

// Add 返回两个整数的和
func Add(a, b int) int {
    return a + b
}

嗯,就这么简单。但你别小看它,我在项目中遇到过更离谱的事——有人写的加法函数,在边界条件下居然溢出了。所以测试不是走过场,是真能帮你发现问题。

我的习惯:被测函数尽量放在独立的包中,比如叫 mathutilcalculator。这样测试代码和被测试代码分离,结构更清晰。

创建_test.go文件:命名有讲究

Go的测试文件命名规则很严格,但也很简单:文件名必须以 _test.go 结尾。比如 math_test.goadd_test.go 都行。

为什么这么设计?你想想看,Go编译器看到 _test.go 就知道这是测试文件,构建时直接跳过,只在运行 go test 时才编译。这比某些语言用注解或配置文件来标记测试文件,要清爽得多。

来看一个完整的测试文件:

// math_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

这里有几个关键点:

  • 包名:测试文件可以和被测文件在同一个包(package main),也可以放在 package main_test。我建议初学者先用同包测试,简单直接。
  • 导入testing包:这是Go标准库提供的测试框架,所有测试函数都依赖它。
  • 函数签名:测试函数必须以 Test 开头,参数是 *testing.T。这是硬性规定,少一个都不行。
我曾经踩过的坑:函数名写成 testAdd(小写t),结果运行 go test 时这个测试被静默忽略了。Go只识别以 Test 开头的导出函数。记住,是 Test,不是 test

使用go test运行测试:见证奇迹

现在打开终端,进入包含 math.gomath_test.go 的目录,执行:

go test

你会看到类似这样的输出:

PASS
ok      example/math    0.002s

如果测试失败,比如我把 Add 函数故意写错:

func Add(a, b int) int {
    return a + b + 1  // 故意加1
}

再运行 go test

--- FAIL: TestAdd (0.00s)
    math_test.go:10: Add(2, 3) = 6; want 5
FAIL
exit status 1
FAIL    example/math    0.002s

看到了吗?测试框架会精确告诉你:哪个测试失败了、在文件的哪一行、实际值和期望值分别是多少。这就是TDD的反馈循环——写测试、跑测试、看失败、改代码、再跑测试。

理解PASS与FAIL输出:别只看颜色

很多人看到绿色PASS就开心,看到红色FAIL就紧张。其实你应该反过来——看到FAIL才应该兴奋,因为测试帮你抓到了问题。

我们来拆解一下输出信息:

输出内容 含义
PASS 所有测试通过,没有调用 t.Errort.Fatal
FAIL 至少有一个测试失败了
ok 测试编译通过且全部PASS
exit status 1 进程退出码为1,表示有错误
0.002s 测试执行耗时,单位秒

这里有个细节:t.Errorft.Fatalf 的区别。前者会记录失败但继续执行当前测试函数,后者会立即终止。我一般用 Errorf,因为我想看到同一个测试函数里所有失败点,而不是第一个失败就停住。

实战建议:运行 go test -v 可以查看每个测试的详细输出。这在调试时特别有用,能看到每个测试的名字和结果。我个人习惯在开发阶段一直开着 -v 模式。

更多测试场景:不止是相等判断

刚才的例子只是判断相等,但实际测试中你会遇到各种情况。比如:

func TestAddNegative(t *testing.T) {
    result := Add(-1, -2)
    expected := -3

    if result != expected {
        t.Errorf("Add(-1, -2) = %d; want %d", result, expected)
    }
}

func TestAddZero(t *testing.T) {
    result := Add(0, 5)
    expected := 5

    if result != expected {
        t.Errorf("Add(0, 5) = %d; want %d", result, expected)
    }
}

你看,测试覆盖了正数、负数、零。这就是边界值分析——测试不仅要测正常情况,更要测边界和异常情况。我在项目中见过一个日期处理函数,所有正常日期都通过,但到了2月29日就崩了。就是因为测试没覆盖闰年这个边界。

总结:第一个测试的要点

  • 被测函数要简单、可预测,方便验证
  • 测试文件必须以 _test.go 结尾
  • 测试函数必须以 Test 开头,参数为 *testing.T
  • 使用 go test 运行,用 -v 查看详情
  • PASS不代表代码没问题,FAIL才是真正帮你省时间的信号

嗯,到这里你已经写完了第一个单元测试。别急着往下走,建议你亲手敲一遍代码,故意改错一次,看看FAIL长什么样。只有亲手踩过坑,才能真正记住。

下一章我们会聊测试覆盖率——说白了就是「你的测试到底测了哪些代码」。到时候见。