Go 的测试是语言工具链的一等公民:go test 内建,无需 JUnit 依赖,也没有注解处理器。这一节把社区惯例一次讲清——后面每个项目的 *_test.go 都在用这些模式。
func TestAdd(t *testing.T) {
got := Add(2, 3)
if got != 5 {
t.Errorf("Add(2,3) = %d, want 5", got)
}
}文件名必须以 _test.go 结尾。包名可以是:
- 同包
package foo:可测未导出符号 - 外部包
package foo_test:只测公开 API,更接近真实调用方
本教程项目多用同包,方便测内部辅助函数。
tests := []struct {
name string
in string
want int
ok bool
}{
{"empty", "", 0, false},
{"number", "42", 42, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.in)
if tt.ok && err != nil {
t.Fatalf("unexpected err: %v", err)
}
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}对比 @ParameterizedTest:没有反射魔法,就是一个切片 + t.Run。失败时子测试名字会出现在输出里,定位很快。
func newServer(t *testing.T) *httptest.Server {
t.Helper()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return srv
}
dir := t.TempDir() // 测试结束自动删t.Helper() 让失败行号指到调用方。t.TempDir() 替代手写临时目录清理。
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"ok":true}`)
}))
defer srv.Close()
resp, err := http.Get(srv.URL + "/ping")Handler 层测试也可以不启服务器,直接:
req := httptest.NewRequest(http.MethodGet, "/bookmarks/1", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)Part 5 的 API 项目会大量使用这两种方式。
func BenchmarkParse(b *testing.B) {
for b.Loop() { // Go 1.24+,比 for i := 0; i < b.N; i++ 更不易被优化掉
Parse("42")
}
}go test -bench=BenchmarkParse -benchmem ./...对比 JMH:没有 warmup 注解套件,但 -count、-benchtime、-cpu 已经够用。项目 09 用它对比三种锁策略。
func FuzzParse(f *testing.F) {
f.Add("42")
f.Fuzz(func(t *testing.T, s string) {
_, _ = Parse(s) // 不应 panic
})
}go test -fuzz=FuzzParse -fuzztime=10s对比 jazzer:内建、与 go test 一体。发现的坏输入会落盘到 testdata/fuzz/,成为永久回归用例。
go test -race ./...
go vet ./...
golangci-lint run并发项目(Part 3)一律带 -race。CI 里本仓库用 scripts/check.sh 统一跑 fmt / build / vet / lint / solution 测试。
| 点 | JUnit 5 | Go testing |
|---|---|---|
| 断言库 | AssertJ / Hamcrest | 手写 if + t.Errorf(或 testify) |
| 参数化 | 注解 | table-driven 切片 |
| 生命周期 | @BeforeEach |
普通辅助函数 + t.Cleanup |
| Mock | Mockito | 接口 + 手写 fake(更常见) |
| 覆盖率 | JaCoCo | go test -cover |
Go 社区对「断言库 / mock 框架」比 Java 克制得多:能用接口 + 假实现就不用 mock 生成器。本教程项目也遵循这个风格。
- 骨架的测试会失败——那是题目,不是坏掉的仓库
solution/的测试必须全绿——CI 只卡 solution- 并发相关加
-race - 确定性优先:用
fstest.MapFS、屏障 channel,少用time.Sleep碰运气
go test -coverprofile=cover.out ./...
go tool cover -func=cover.out
go tool cover -html=cover.out100% 覆盖率只说明每条语句被执行过,不说明断言正确。一个什么都不检查的测试也可以覆盖所有代码。覆盖率门槛的价值是防止大块新逻辑完全没测,而不是替代评审测试语义。
优先用 channel 构造可观察的时序:
started := make(chan struct{})
release := make(chan struct{})
go func() {
close(started)
<-release
}()
<-started
// 此时可以确定 goroutine 已进入指定状态
close(release)time.Sleep 只能说“大概等够了”,慢机器和 race detector 会让这种测试变得脆弱。如果必须等待,使用带超时的 select,不要无限阻塞。
- 内存 Store 或
fstest.MapFS:便宜、确定,适合单元测试。 httptest.Server:需要真实 HTTP 客户端行为时使用。httptest.ResponseRecorder:只测 Handler 边界时更快。- 真实 SQLite
:memory::当 SQL 本身就是契约时,比 mockdatabase/sql更有价值。
- 把一个使用五次
time.Sleep的并发测试改为 channel 屏障。 - 为 HTTP Handler 分别写 Recorder 测试和
httptest.Server测试,解释各自多覆盖了什么。 - 写一个只覆盖语句但没有有效断言的测试,再通过改错生产代码证明它无法捕获回归。
上一节:CLI 与 slog | 下一部分:HTTP 服务基础