Files
zhuangang/topics/backend/go/03-testing-engineering.md
T

101 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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` 的作用。