--- id: backend.go.testing-engineering title: 测试和工程化:表格驱动测试、httptest、模块、项目结构 type: lesson domain: backend stack: [go] tags: [Go, 测试, httptest, go mod, 项目结构] level: intermediate prerequisites: [backend.go.basics-types] est_hours: 3 related: [backend.go.interview, backend.gin.routing-binding] week: [1, 2, 3] order: 3 --- # 测试和工程化:表格驱动测试、httptest、模块、项目结构 ## 解释说明 - **go test**:`xxx_test.go` 里的 `TestXxx(t *testing.T)` 会被自动执行。`t.Run` 定义子测试;`-race` 检测数据竞争;`-cover` 看覆盖率;`BenchmarkXxx` 做性能测试。 - **表格驱动测试**:把多组“输入 + 期望”写成切片循环执行,是 Go 社区的惯例,新增用例只加一行。 - **httptest**:`httptest.NewRecorder()` 不启动端口直接测 handler;`httptest.NewServer` 启动一个真实的本地服务,用来模拟第三方接口。 - **模块**:`go mod init 模块名` 生成 `go.mod`;`go get` 加依赖;`go mod tidy` 清理。`go.sum` 记录依赖的校验和,要提交。 - **项目结构**(社区常见做法,不是强制标准):`cmd/<程序名>/main.go` 放入口;`internal/` 放不希望被其他模块导入的代码(编译器强制);按职责分包(`handler`、`service`、`store`),避免一个 `utils` 包装所有东西。 ## 案例 接口层的测试一般这样分:handler 测试用 `httptest.NewRecorder` + 内存版 store,覆盖参数校验、状态码、鉴权;store 的集成测试连真实数据库(没有数据库时跳过);调用第三方的代码用 `httptest.NewServer` 模拟各种返回(超时、500、格式错误)。 ## 代码示例 ```go // 文件:slug.go package slug import ( "strings" "unicode" ) // Make 把标题转成 URL 片段:小写、空白和标点变成 -,去掉首尾 - func Make(title string) string { var b strings.Builder dash := false for _, r := range strings.ToLower(title) { if unicode.IsLetter(r) || unicode.IsDigit(r) { b.WriteRune(r) dash = false } else if !dash && b.Len() > 0 { b.WriteByte('-') dash = true } } return strings.TrimSuffix(b.String(), "-") } ``` ```go // 文件:slug_test.go package slug import "testing" func TestMake(t *testing.T) { cases := []struct { name, in, want string }{ {"普通", "Hello World", "hello-world"}, {"多个符号", "Go -- 并发!!", "go-并发"}, {"首尾空白", " Redis ", "redis"}, {"空", "", ""}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := Make(c.in); got != c.want { t.Fatalf("Make(%q) = %q, want %q", c.in, got, c.want) } }) } } func BenchmarkMake(b *testing.B) { for b.Loop() { // Go 1.24+ 的写法;旧版本用 for i := 0; i < b.N; i++ Make("Hello World From Go") } } ``` 运行:`go mod init slug && go test -v -race -cover ./... && go test -bench=. ./...` ## 易错点 - 测试之间共享全局状态(全局 map、同一张表)会让测试结果依赖执行顺序;每个测试自己准备数据,`t.Cleanup` 清理。 - 只测“正常路径”:错误路径(参数错、下游失败、超时)才是线上出问题的地方。 - `internal` 包只能被它的父目录及子目录导入;别的模块导入会编译失败——这是故意的。 - 不提交 `go.sum` 会让别人构建时无法校验依赖。 ## 面试常问 见 [Go 面试题](interview.md):怎么测试依赖数据库或第三方接口的代码、`internal` 的作用。