编写第一个单元测试:从零到PASS
好,咱们直接动手。这一章的目标很简单——写一个真正的单元测试,跑起来,看懂输出。我见过太多人把测试想得太玄乎,其实说白了,测试就是一段代码,用来验证另一段代码的行为对不对。
被测函数设计:先有被测试的代码
写测试之前,总得有个被测对象吧?我个人习惯先写一个简单的工具函数,这样能聚焦在测试本身,不会被业务逻辑干扰。
假设我们要写一个加法函数:
// math.go
package main
// Add 返回两个整数的和
func Add(a, b int) int {
return a + b
}
嗯,就这么简单。但你别小看它,我在项目中遇到过更离谱的事——有人写的加法函数,在边界条件下居然溢出了。所以测试不是走过场,是真能帮你发现问题。
mathutil 或 calculator。这样测试代码和被测试代码分离,结构更清晰。
创建_test.go文件:命名有讲究
Go的测试文件命名规则很严格,但也很简单:文件名必须以 _test.go 结尾。比如 math_test.go、add_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.go 和 math_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.Error 或 t.Fatal |
FAIL |
至少有一个测试失败了 |
ok |
测试编译通过且全部PASS |
exit status 1 |
进程退出码为1,表示有错误 |
0.002s |
测试执行耗时,单位秒 |
这里有个细节:t.Errorf 和 t.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长什么样。只有亲手踩过坑,才能真正记住。
下一章我们会聊测试覆盖率——说白了就是「你的测试到底测了哪些代码」。到时候见。