mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: go-testing
|
||||
description: Use when writing, reviewing, or improving Go test code — including table-driven tests, subtests, parallel tests, test helpers, test doubles, and assertions with cmp.Diff. Also use when a user asks to write a test for a Go function, even if they don't mention specific patterns like table-driven tests or subtests. Does not cover benchmark performance testing (see go-performance).
|
||||
license: Apache-2.0
|
||||
compatibility: Uses github.com/google/go-cmp for cmp.Diff comparisons
|
||||
metadata:
|
||||
sources: "Google Style Guide, Uber Style Guide"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go 测试
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 使用场景 |
|
||||
|------|----------|
|
||||
| `t.Error` | 默认 — 报告失败,继续运行 |
|
||||
| `t.Fatal` | 设置失败或继续运行没有意义 |
|
||||
| `cmp.Diff` | 比较 struct、slice、map、proto |
|
||||
| 表驱动 | 多个用例共享相同逻辑 |
|
||||
| 子测试 | 需要过滤、并行执行或命名 |
|
||||
| `t.Helper()` | 任何测试辅助函数(作为第一条语句调用) |
|
||||
| `t.Cleanup()` | 在辅助函数中进行清理,替代 defer |
|
||||
|
||||
---
|
||||
|
||||
## 有用的测试失败信息
|
||||
|
||||
> **规范**:测试失败必须在不阅读测试源码的情况下可诊断。
|
||||
|
||||
每条失败信息必须包含:函数名、输入、实际值(got)和期望值(want)。使用格式 `YourFunc(%v) = %v, want %v`。
|
||||
|
||||
```go
|
||||
// 好:
|
||||
t.Errorf("Add(2, 3) = %d, want %d", got, 5)
|
||||
|
||||
// 不好:缺少函数名和输入
|
||||
t.Errorf("got %d, want %d", got, 5)
|
||||
```
|
||||
|
||||
始终先打印 got 再打印 want:`got %v, want %v` — 绝不反转。
|
||||
|
||||
---
|
||||
|
||||
## 不使用断言库
|
||||
|
||||
> **规范**:不要使用断言库。对于复杂比较使用 `cmp.Diff`。
|
||||
|
||||
```go
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("GetPost() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
```
|
||||
|
||||
对于 protocol buffers,添加 `protocmp.Transform()` 作为 cmp 选项。始终在 diff 信息中包含方向键 `(-want +got)`。避免比较 JSON/序列化输出 — 改为语义比较。
|
||||
|
||||
> 在编写自定义比较辅助函数或领域特定测试工具时,请阅读 [references/TEST-HELPERS.md](references/TEST-HELPERS.md)。
|
||||
|
||||
---
|
||||
|
||||
## t.Error vs t.Fatal
|
||||
|
||||
> **规范**:默认使用 `t.Error` 以在一次运行中报告所有失败。仅在无法继续时使用 `t.Fatal`。
|
||||
|
||||
**选择 `t.Fatal` 的场景:**
|
||||
- 设置失败(数据库连接、文件加载)
|
||||
- 下一个断言依赖于上一个断言成功(例如,编码后的解码)
|
||||
|
||||
**绝不在测试 goroutine 以外的 goroutine 中调用 `t.Fatal`/`t.FailNow`** — 改为使用 `t.Error`。
|
||||
|
||||
> 在编写需要在 t.Error 和 t.Fatal 之间选择的辅助函数时,或需要两者的详细示例时,请阅读 [references/TEST-HELPERS.md](references/TEST-HELPERS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 表驱动测试
|
||||
|
||||
> 在搭建新的表驱动测试并需要标准的 struct、循环和子测试布局时,请参阅 `assets/table-test-template.go`。
|
||||
|
||||
> **建议**:当多个用例共享相同逻辑时使用表驱动测试。
|
||||
|
||||
**使用表测试的场景:** 所有用例运行相同的代码路径,没有条件设置、mock 或断言。单个 `shouldErr` bool 是可以接受的。
|
||||
|
||||
**不使用表测试的场景:** 用例需要复杂设置、条件 mock 或多个分支 — 改为编写单独的测试函数。
|
||||
|
||||
**关键规则:**
|
||||
- 当用例跨越多行或有相同类型的相邻字段时,使用字段名
|
||||
- 在失败信息中包含输入 — 绝不通过索引标识行
|
||||
|
||||
> 在编写表驱动测试、子测试或并行测试时,请阅读 [references/TABLE-DRIVEN-TESTS.md](references/TABLE-DRIVEN-TESTS.md)。
|
||||
|
||||
> **验证**:在生成或修改测试后,运行 `go test -run TestXxx -v` 验证测试能编译并通过。在继续之前修复任何编译错误。
|
||||
|
||||
---
|
||||
|
||||
## 测试辅助函数
|
||||
|
||||
> **规范**:测试辅助函数必须首先调用 `t.Helper()` 并使用 `t.Cleanup()` 进行清理。
|
||||
|
||||
```go
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not open database: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
> 在编写测试辅助函数、清理函数或自定义比较工具时,请阅读 [references/TEST-HELPERS.md](references/TEST-HELPERS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 测试错误语义
|
||||
|
||||
> **建议**:测试错误语义,而非错误消息字符串。
|
||||
|
||||
```go
|
||||
// 不好:脆弱的字符串比较
|
||||
if err.Error() != "invalid input" { ... }
|
||||
|
||||
// 好:语义检查
|
||||
if !errors.Is(err, ErrInvalidInput) { ... }
|
||||
```
|
||||
|
||||
对于不需要特定语义的简单存在性检查:
|
||||
|
||||
```go
|
||||
if gotErr := err != nil; gotErr != tt.wantErr {
|
||||
t.Errorf("f(%v) error = %v, want error presence = %t", tt.input, err, tt.wantErr)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试组织
|
||||
|
||||
> 在使用测试替身、选择测试包位置或规划测试设置范围时,请阅读 [references/TEST-ORGANIZATION.md](references/TEST-ORGANIZATION.md)。
|
||||
|
||||
> 在设计可重用的测试验证函数时,请阅读 [references/VALIDATION-APIS.md](references/VALIDATION-APIS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 集成测试
|
||||
|
||||
> 在编写 TestMain、验收测试或需要真实 HTTP/RPC 传输层的测试时,请阅读 [references/INTEGRATION.md](references/INTEGRATION.md)。
|
||||
|
||||
---
|
||||
|
||||
## 可用脚本
|
||||
|
||||
- **`scripts/gen-table-test.sh`** — 生成表驱动测试脚手架
|
||||
|
||||
```bash
|
||||
bash scripts/gen-table-test.sh ParseConfig config > config/parse_config_test.go
|
||||
bash scripts/gen-table-test.sh --parallel ParseConfig config # 带 t.Parallel()
|
||||
bash scripts/gen-table-test.sh --output config/parse_config_test.go ParseConfig config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关 Skill
|
||||
|
||||
- **错误测试**:在使用 `errors.Is`/`errors.As` 或哨兵错误测试错误语义时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **接口 mock**:在消费端通过实现接口创建测试替身时,请参阅 [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- **测试函数命名**:在命名测试函数、子测试或测试辅助工具时,请参阅 [go-naming](../go-naming/SKILL.md)
|
||||
- **Linter 集成**:在 CI 或 pre-commit hooks 中与测试一起运行 linter 时,请参阅 [go-linting](../go-linting/SKILL.md)
|
||||
@@ -0,0 +1,30 @@
|
||||
package example_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
// TODO: add input fields
|
||||
// TODO: add expected output fields
|
||||
}{
|
||||
{
|
||||
name: "basic case",
|
||||
// TODO: fill in
|
||||
},
|
||||
{
|
||||
name: "edge case",
|
||||
// TODO: fill in
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// TODO: call function under test
|
||||
// TODO: compare got vs want
|
||||
// if diff := cmp.Diff(want, got); diff != "" {
|
||||
// t.Errorf("Example() mismatch (-want +got):\n%s", diff)
|
||||
// }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
# Go 测试:集成和高级模式
|
||||
|
||||
TestMain、验收测试和真实传输层测试的详细参考。
|
||||
来源:Google Go Style Guide(最佳实践)。
|
||||
|
||||
---
|
||||
|
||||
## TestMain
|
||||
|
||||
> **来源**:Google Go Style Guide(最佳实践)
|
||||
|
||||
当 **包中的所有测试** 都需要共同的设置且需要清理时(例如,共享数据库),使用 `func TestMain(m *testing.M)`。这 **不应该是你的首选** — 尽可能优先使用作用域测试辅助函数或 `t.Cleanup`。
|
||||
|
||||
```go
|
||||
var db *sql.DB
|
||||
|
||||
func TestInsert(t *testing.T) { /* 使用 db */ }
|
||||
func TestSelect(t *testing.T) { /* 使用 db */ }
|
||||
|
||||
func runMain(ctx context.Context, m *testing.M) (code int, err error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
d, err := setupDatabase(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer d.Close()
|
||||
db = d
|
||||
|
||||
return m.Run(), nil
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code, err := runMain(context.Background(), m)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// defer 语句在 os.Exit 之后不会执行
|
||||
os.Exit(code)
|
||||
}
|
||||
```
|
||||
|
||||
关键点:
|
||||
- 将设置提取到辅助函数(`runMain`)中,使 `defer` 能正确工作
|
||||
- 通过 `log.Fatal` 将失败信息写入 stderr
|
||||
- 确保各个测试用例保持独立 — 重置它们修改的任何全局状态
|
||||
|
||||
---
|
||||
|
||||
## 验收测试
|
||||
|
||||
> **来源**:Google Go Style Guide(最佳实践)
|
||||
|
||||
验收测试验证实现是否遵循契约,将其视为黑盒。当用户实现你的接口并且你想提供可重用的验证套件时,这种模式很有用。
|
||||
|
||||
### 结构
|
||||
|
||||
1. 创建测试辅助包(例如,为 `chess` 包创建 `chesstest`)
|
||||
2. 导出一个接受被测实现的验证函数:
|
||||
|
||||
```go
|
||||
// Package chesstest 为 chess.Player 实现提供验收测试。
|
||||
package chesstest
|
||||
|
||||
// ExercisePlayer 在单回合中测试 Player 实现。
|
||||
// 如果玩家走了正确的一步,返回 nil,否则返回描述违规行为的错误。
|
||||
func ExercisePlayer(b *chess.Board, p chess.Player) error {
|
||||
move := p.Move()
|
||||
if putsOwnKingIntoCheck(b, move) {
|
||||
return &IllegalMoveError{Move: move, Reason: "puts own king in check"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
3. 最终用户针对验证函数编写简单测试:
|
||||
|
||||
```go
|
||||
func TestAcceptance(t *testing.T) {
|
||||
player := deepblue.New()
|
||||
if err := chesstest.ExerciseGame(t, chesstest.SimpleGame, player); err != nil {
|
||||
t.Errorf("Deep Blue player failed acceptance test: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
仅在设置失败时使用 `t.Fatal` — 验证错误应该返回,而非 fatal。
|
||||
|
||||
---
|
||||
|
||||
## 使用真实传输层
|
||||
|
||||
> **来源**:Google Go Style Guide(最佳实践)
|
||||
|
||||
在测试基于 HTTP 或 RPC 的组件集成时,优先使用真实传输层往返而非手动实现的客户端 mock:
|
||||
|
||||
```go
|
||||
func TestAPIIntegration(t *testing.T) {
|
||||
// 使用假后端启动测试服务器
|
||||
srv := httptest.NewServer(newFakeHandler())
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
// 对测试服务器使用真实 HTTP 客户端
|
||||
client := api.NewClient(srv.URL)
|
||||
result, err := client.GetUser(context.Background(), "user-123")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser() error: %v", err)
|
||||
}
|
||||
if result.Name != "Test User" {
|
||||
t.Errorf("GetUser().Name = %q, want %q", result.Name, "Test User")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用生产客户端配合测试服务器,可以确保测试尽可能多地覆盖真实代码,避免模拟客户端行为的复杂性。
|
||||
|
||||
---
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 在 TestMain 中直接调用 os.Exit
|
||||
|
||||
`os.Exit` 立即终止进程 — defer 的清理函数永远不会执行。将设置/清理提取到辅助函数中,使 `defer` 能正确工作:
|
||||
|
||||
```go
|
||||
// 不好:defer 不会执行
|
||||
func TestMain(m *testing.M) {
|
||||
setup()
|
||||
defer cleanup()
|
||||
os.Exit(m.Run()) // cleanup() 永远不会执行
|
||||
}
|
||||
|
||||
// 好:提取到辅助函数中,使 defer 在 os.Exit 之前执行
|
||||
func runTests(m *testing.M) int {
|
||||
setup()
|
||||
defer cleanup()
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(runTests(m))
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,154 @@
|
||||
# 表驱动测试、子测试和并行测试
|
||||
|
||||
在 Go 中组织表驱动测试和子测试的详细参考。
|
||||
来源:Google Go Style Guide、Uber Go Style Guide。
|
||||
|
||||
---
|
||||
|
||||
## 基本结构
|
||||
|
||||
```go
|
||||
func TestCompare(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"", "", 0},
|
||||
{"a", "", 1},
|
||||
{"", "a", -1},
|
||||
{"abc", "abc", 0},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := Compare(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("Compare(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
当测试用例跨越多行或有相同类型的相邻字段时,**使用字段名**:
|
||||
|
||||
```go
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want int
|
||||
}{
|
||||
{name: "empty", input: "", want: 0},
|
||||
{name: "single", input: "a", want: 1},
|
||||
}
|
||||
```
|
||||
|
||||
**不要通过索引标识行** — 在失败信息中包含输入,而非使用 `Case #%d failed`。
|
||||
|
||||
---
|
||||
|
||||
## 避免表测试中的复杂性
|
||||
|
||||
当测试用例需要复杂设置、条件 mock 或多个分支时,优先使用单独的测试函数而非表测试。
|
||||
|
||||
```go
|
||||
// 不好:太多条件字段使测试难以理解
|
||||
tests := []struct {
|
||||
give string
|
||||
want string
|
||||
wantErr error
|
||||
shouldCallX bool
|
||||
shouldCallY bool
|
||||
giveXResponse string
|
||||
giveXErr error
|
||||
giveYResponse string
|
||||
giveYErr error
|
||||
}{...}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.give, func(t *testing.T) {
|
||||
if tt.shouldCallX {
|
||||
xMock.EXPECT().Call().Return(tt.giveXResponse, tt.giveXErr)
|
||||
}
|
||||
if tt.shouldCallY {
|
||||
yMock.EXPECT().Call().Return(tt.giveYResponse, tt.giveYErr)
|
||||
}
|
||||
// ...
|
||||
})
|
||||
}
|
||||
|
||||
// 好:单独的专注测试更清晰
|
||||
func TestShouldCallX(t *testing.T) {
|
||||
xMock.EXPECT().Call().Return("XResponse", nil)
|
||||
got, err := DoComplexThing("inputX", xMock, yMock)
|
||||
// 断言...
|
||||
}
|
||||
|
||||
func TestShouldCallYAndFail(t *testing.T) {
|
||||
yMock.EXPECT().Call().Return("YResponse", nil)
|
||||
_, err := DoComplexThing("inputY", xMock, yMock)
|
||||
// 断言错误...
|
||||
}
|
||||
```
|
||||
|
||||
**表测试最适合以下场景:**
|
||||
|
||||
- 所有用例运行相同逻辑(无条件断言)
|
||||
- 所有用例的设置相同
|
||||
- 没有基于测试用例字段的条件 mock
|
||||
- 所有表字段在所有测试中都被使用
|
||||
|
||||
如果测试体短且直接,单个 `shouldErr` 字段用于成功/失败检查是可以接受的。
|
||||
|
||||
---
|
||||
|
||||
## 子测试
|
||||
|
||||
使用 `t.Run` 实现更好的组织、过滤和并行执行。
|
||||
|
||||
### 子测试命名
|
||||
|
||||
- 使用清晰、简洁的名称:`t.Run("empty_input", ...)`、`t.Run("hu_to_en", ...)`
|
||||
- 避免冗长的描述或斜杠(斜杠会破坏测试过滤)
|
||||
- 子测试必须独立 — 不共享状态或执行顺序依赖
|
||||
|
||||
### 带子测试的表测试
|
||||
|
||||
```go
|
||||
func TestTranslate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, srcLang, dstLang, input, want string
|
||||
}{
|
||||
{"hu_en_basic", "hu", "en", "köszönöm", "thank you"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Translate(tt.srcLang, tt.dstLang, tt.input); got != tt.want {
|
||||
t.Errorf("Translate(%q, %q, %q) = %q, want %q",
|
||||
tt.srcLang, tt.dstLang, tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并行测试
|
||||
|
||||
在表测试中使用 `t.Parallel()` 时,注意循环变量捕获:
|
||||
|
||||
```go
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Go 1.22+:tt 在每次迭代中被正确捕获
|
||||
// Go 1.21-:在此处添加 "tt := tt" 来捕获变量
|
||||
got := Process(tt.give)
|
||||
if got != tt.want {
|
||||
t.Errorf("Process(%q) = %q, want %q", tt.give, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,131 @@
|
||||
# 测试辅助函数、断言和比较
|
||||
|
||||
编写测试辅助函数、避免断言库以及在 t.Error 和 t.Fatal 之间选择的详细参考。
|
||||
来源:Google Go Style Guide、Uber Go Style Guide。
|
||||
|
||||
---
|
||||
|
||||
## 测试辅助函数模式
|
||||
|
||||
测试辅助函数必须首先调用 `t.Helper()`,使失败指向调用者。
|
||||
对设置失败使用 `t.Fatal`,对清理使用 `t.Cleanup`。
|
||||
|
||||
```go
|
||||
func mustLoadTestData(t *testing.T, filename string) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Setup failed: could not read %s: %v", filename, err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not open database: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
**关键规则:**
|
||||
- 将 `t.Helper()` 作为第一条语句调用,将失败归因于调用者
|
||||
- 对设置失败使用 `t.Fatal`(不要从辅助函数返回错误)
|
||||
- 使用 `t.Cleanup()` 进行清理而非 defer — 即使测试调用 `t.FailNow` 它也会执行
|
||||
|
||||
---
|
||||
|
||||
## 避免断言库
|
||||
|
||||
> **规范**:不要创建或使用断言库。
|
||||
|
||||
断言库会碎片化开发者体验,并且经常产生无用的失败信息。
|
||||
|
||||
```go
|
||||
// 不好:
|
||||
assert.IsNotNil(t, "obj", obj)
|
||||
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
|
||||
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
|
||||
|
||||
// 好:使用 cmp 包和标准比较
|
||||
want := BlogPost{
|
||||
Type: "blogPost",
|
||||
Comments: 2,
|
||||
Body: "Hello, world!",
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("GetPost() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
```
|
||||
|
||||
### 领域特定比较
|
||||
|
||||
对于领域特定比较,返回值或错误而非调用 `t.Error`:
|
||||
|
||||
```go
|
||||
func postLength(p BlogPost) int { return len(p.Body) }
|
||||
|
||||
func TestBlogPost(t *testing.T) {
|
||||
post := BlogPost{Body: "Hello"}
|
||||
if got, want := postLength(post), 5; got != want {
|
||||
t.Errorf("postLength(post) = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 比较和 Diff
|
||||
|
||||
对于复杂类型,优先使用 `cmp.Equal` 和 `cmp.Diff`。始终在 diff 信息中包含方向键 `(-want +got)`。
|
||||
|
||||
```go
|
||||
// struct 比较
|
||||
want := &Doc{Type: "blogPost", Authors: []string{"isaac", "albert"}}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("AddPost() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// Protocol buffers
|
||||
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
|
||||
t.Errorf("Foo() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
```
|
||||
|
||||
**避免不稳定的比较** — 不要比较可能变化的 JSON/序列化输出。改为语义比较。
|
||||
|
||||
---
|
||||
|
||||
## t.Error vs t.Fatal:详细指南
|
||||
|
||||
使用 `t.Error` 保持测试继续运行,在一次运行中报告所有失败:
|
||||
|
||||
```go
|
||||
// 好:报告所有不匹配
|
||||
if diff := cmp.Diff(wantMean, gotMean); diff != "" {
|
||||
t.Errorf("Mean mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wantVariance, gotVariance); diff != "" {
|
||||
t.Errorf("Variance mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
```
|
||||
|
||||
当后续检查无意义时使用 `t.Fatal`:
|
||||
|
||||
```go
|
||||
gotEncoded := Encode(input)
|
||||
if gotEncoded != wantEncoded {
|
||||
t.Fatalf("Encode(%q) = %q, want %q", input, gotEncoded, wantEncoded)
|
||||
}
|
||||
gotDecoded, err := Decode(gotEncoded)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode(%q) error: %v", gotEncoded, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 不要从 Goroutine 中调用 t.Fatal
|
||||
|
||||
> **规范**:绝不在测试 goroutine 以外的 goroutine 中调用 `t.Fatal`、`t.Fatalf` 或 `t.FailNow`。改为使用 `t.Error` 并让 goroutine 自然返回。
|
||||
@@ -0,0 +1,167 @@
|
||||
# 测试组织参考
|
||||
|
||||
来源:Google Go Style Guide(最佳实践、决策)。
|
||||
|
||||
---
|
||||
|
||||
## 测试替身类型
|
||||
|
||||
| 替身 | 用途 | 有状态? | 验证调用? |
|
||||
|------|------|----------|-----------|
|
||||
| Stub | 返回预设数据 | 否 | 否 |
|
||||
| Fake | 可工作但简化的实现 | 是 | 否 |
|
||||
| Spy | 记录调用以供后续检查 | 是 | 是 |
|
||||
|
||||
**优先使用 fake 而非 mock。** Fake 更具可读性且不需要 mock 框架。仅在验证副作用(例如,分析事件)时使用 spy。
|
||||
|
||||
```go
|
||||
// Fake:可工作的内存实现
|
||||
type FakeUserStore struct {
|
||||
users map[string]*User
|
||||
}
|
||||
|
||||
func (f *FakeUserStore) GetUser(id string) (*User, error) {
|
||||
u, ok := f.users[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Spy:记录调用以供后续断言
|
||||
type SpyEmailSender struct{ Sent []string }
|
||||
|
||||
func (s *SpyEmailSender) Send(to, body string) error {
|
||||
s.Sent = append(s.Sent, to)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试替身命名约定
|
||||
|
||||
> **建议**:为测试替身(stub、fake、spy)遵循一致的命名。
|
||||
|
||||
**包命名**:在生产代码旁边创建一个 `*test` 包(例如,为 `creditcard` 包创建 `creditcardtest`,为独立的 fake 服务创建 `fakeauthservice`)。
|
||||
|
||||
```go
|
||||
// 好:在 creditcardtest 包中
|
||||
|
||||
// 单个替身 — 使用简单名称
|
||||
type Stub struct{}
|
||||
func (Stub) Charge(*creditcard.Card, money.Money) error { return nil }
|
||||
|
||||
// 多种行为 — 按行为命名
|
||||
type AlwaysCharges struct{}
|
||||
type AlwaysDeclines struct{}
|
||||
|
||||
// 多种类型 — 包含类型名
|
||||
type StubService struct{}
|
||||
type StubStoredValue struct{}
|
||||
```
|
||||
|
||||
**局部变量**:为测试替身变量添加替身类型前缀,使调用处更清晰:
|
||||
|
||||
```go
|
||||
// 好:替身类型立即可见
|
||||
spyCC := &creditcardtest.Spy{}
|
||||
stubDB := &dbtest.Stub{Balance: 100}
|
||||
|
||||
// 不好:模糊 — 这是真实的还是替身?
|
||||
cc := &creditcardtest.Spy{}
|
||||
db := &dbtest.Stub{Balance: 100}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 独立测试辅助包
|
||||
|
||||
当多个包需要相同的替身、辅助函数有足够的逻辑需要自己的测试、或者你想为接口实现者提供验收测试套件时,创建独立的测试辅助包。
|
||||
|
||||
| 模式 | 使用场景 | 示例 |
|
||||
|------|----------|------|
|
||||
| `footest` | `foo` 包的通用测试辅助 | `creditcardtest`、`usertest` |
|
||||
| `fakeX` | 独立的 fake 服务包 | `fakeauthservice`、`fakestorage` |
|
||||
|
||||
```go
|
||||
package usertest
|
||||
|
||||
func NewFakeStore(t *testing.T, users ...*user.User) *FakeUserStore {
|
||||
t.Helper()
|
||||
store := &FakeUserStore{users: make(map[string]*user.User)}
|
||||
for _, u := range users {
|
||||
store.users[u.ID] = u
|
||||
}
|
||||
return store
|
||||
}
|
||||
```
|
||||
|
||||
导出接受 `*testing.T` 的构造函数,以便调用 `t.Helper()` 和 `t.Cleanup()`。
|
||||
|
||||
---
|
||||
|
||||
## 测试包
|
||||
|
||||
| 包声明 | 使用场景 |
|
||||
|--------|----------|
|
||||
| `package foo` | 同包测试,可以访问非导出标识符 |
|
||||
| `package foo_test` | 黑盒测试,避免循环依赖 |
|
||||
|
||||
两者都放在同一目录下的 `foo_test.go` 文件中。
|
||||
|
||||
**使用 `package foo`(白盒)** 当你需要测试非导出函数或内部状态时。
|
||||
|
||||
**使用 `package foo_test`(黑盒)** 当仅测试公共 API、打破导入循环或验证外部可用性时。
|
||||
|
||||
```go
|
||||
package parser_test // 黑盒:仅测试导出的 API
|
||||
|
||||
import "mymodule/parser"
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
got, err := parser.Parse("input")
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
如果黑盒测试需要非导出符号,在 `package foo`(非 `foo_test`)中创建 `export_test.go` 来暴露它。谨慎使用。
|
||||
|
||||
---
|
||||
|
||||
## 设置作用域
|
||||
|
||||
> **建议**:保持设置仅限于需要它的测试。
|
||||
|
||||
每个测试中的显式设置更清晰,避免惩罚不相关的测试:
|
||||
|
||||
```go
|
||||
// 好:在需要它的测试中显式设置
|
||||
func TestParseData(t *testing.T) {
|
||||
data := mustLoadDataset(t)
|
||||
// ...
|
||||
}
|
||||
|
||||
func TestUnrelated(t *testing.T) {
|
||||
// 不需要为数据集加载付出代价
|
||||
}
|
||||
```
|
||||
|
||||
**避免使用全局 `init` 进行测试设置** — 它会对文件中的每个测试运行,即使是不相关的测试。
|
||||
|
||||
**子测试设置**:当一组子测试共享设置时,使用带 `t.Run` 的父测试:
|
||||
|
||||
```go
|
||||
func TestDatabase(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
t.Run("Insert", func(t *testing.T) {
|
||||
// 使用 db
|
||||
})
|
||||
t.Run("Select", func(t *testing.T) {
|
||||
// 使用 db
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
这将数据库的生命周期限定在需要它的子测试范围内。仅在万不得已时使用 `TestMain`(参见 [INTEGRATION.md](INTEGRATION.md))。
|
||||
@@ -0,0 +1,108 @@
|
||||
# 可扩展验证 API
|
||||
|
||||
设计可重用测试验证函数的详细参考,调用者可以将其用于验收测试。来源:Google Go Style Guide(最佳实践)。
|
||||
|
||||
---
|
||||
|
||||
## `*test` 包导出模式
|
||||
|
||||
当你拥有一个由他人实现的接口时,在配套的 `*test` 包中导出一个验证函数。这使实现者无需复制你的测试逻辑即可验证正确性。
|
||||
|
||||
```go
|
||||
// Package storagetest 为 storage.Backend 提供验收测试。
|
||||
package storagetest
|
||||
|
||||
// Verify 对任何 storage.Backend 运行验证套件。
|
||||
// 返回描述第一个违规行为的错误,成功时返回 nil。
|
||||
func Verify(b storage.Backend) error {
|
||||
if err := verifyRoundTrip(b); err != nil {
|
||||
return fmt.Errorf("round-trip: %w", err)
|
||||
}
|
||||
if err := verifyNotFound(b); err != nil {
|
||||
return fmt.Errorf("not-found: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
调用者编写一个薄测试来接入他们的实现:
|
||||
|
||||
```go
|
||||
func TestMyBackend(t *testing.T) {
|
||||
b := mybackend.New(t)
|
||||
if err := storagetest.Verify(b); err != nil {
|
||||
t.Errorf("MyBackend failed acceptance: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计可扩展的验证函数
|
||||
|
||||
**返回错误,而非 `*testing.T` 失败。** 这使验证函数可作为普通 Go 函数使用 — 调用者决定违规是 `t.Error` 还是 `t.Fatal`。
|
||||
|
||||
```go
|
||||
// 好:返回错误 — 调用者控制测试流程
|
||||
func ExercisePlayer(b *chess.Board, p chess.Player) error {
|
||||
move := p.Move()
|
||||
if putsOwnKingIntoCheck(b, move) {
|
||||
return &IllegalMoveError{Move: move, Reason: "puts own king in check"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 不好:调用 t.Fatal — 调用者失去控制
|
||||
func ExercisePlayer(t *testing.T, b *chess.Board, p chess.Player) {
|
||||
t.Helper()
|
||||
move := p.Move()
|
||||
if putsOwnKingIntoCheck(b, move) {
|
||||
t.Fatalf("illegal move: %v puts own king in check", move)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**在需要丰富诊断信息时使用自定义错误类型**:
|
||||
|
||||
```go
|
||||
type IllegalMoveError struct {
|
||||
Move chess.Move
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *IllegalMoveError) Error() string {
|
||||
return fmt.Sprintf("illegal move %v: %s", e.Move, e.Reason)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 何时使用验证 API vs 简单辅助函数
|
||||
|
||||
| 场景 | 使用方式 |
|
||||
|------|----------|
|
||||
| 你拥有的接口,由他人实现 | `*test` 包中的验证 API |
|
||||
| 同一包中跨测试共享设置 | 使用 `t.Helper()` 的测试辅助函数 |
|
||||
| 在 2-3 个测试中重用的复杂断言 | 返回 `error` 或 `bool` 的辅助函数 |
|
||||
| 一次性的设置或比较 | 内联测试代码 |
|
||||
|
||||
**验证 API** 在以下场景值得额外的包:
|
||||
- 多个外部包将实现你的接口
|
||||
- 契约有容易被忽略的非显而易见的不变量
|
||||
- 你想为"正确行为"提供单一事实来源
|
||||
|
||||
**简单辅助函数** 在以下场景更好:
|
||||
- 辅助函数是直接的设置或比较函数
|
||||
- 重用是偶然的,不是已发布契约的一部分
|
||||
|
||||
---
|
||||
|
||||
## 命名约定
|
||||
|
||||
使用表示范围的动词命名函数:`Verify`、`Exercise`、`RunConformance`。接受被测接口作为参数 — 绝不在验证包内部构造实现。
|
||||
|
||||
| 包 | 函数 | 用途 |
|
||||
|----|------|------|
|
||||
| `storagetest` | `Verify` | 验证 `storage.Backend` |
|
||||
| `chesstest` | `ExercisePlayer` | 验证 `chess.Player` |
|
||||
| `cachetest` | `RunConformance` | `cache.Cache` 的完整一致性套件 |
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Generate a table-driven test scaffold for a Go function
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] <FuncName> <package>
|
||||
|
||||
DESCRIPTION
|
||||
Outputs a table-driven test file for the given function and package.
|
||||
By default writes to stdout; use --output to write to a file.
|
||||
|
||||
Exits 0 on success, 2 on error.
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
--output FILE Write to FILE instead of stdout
|
||||
--force Allow --output to overwrite an existing file
|
||||
--parallel Include t.Parallel() in generated test
|
||||
--json Output structured JSON metadata to stdout
|
||||
|
||||
ARGUMENTS
|
||||
FuncName Name of the function to test (must be exported/uppercase)
|
||||
package Go package name for the test file
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME ParseConfig config
|
||||
bash $SCRIPT_NAME --parallel ParseConfig config
|
||||
bash $SCRIPT_NAME --output config/parse_config_test.go ParseConfig config
|
||||
bash $SCRIPT_NAME --force --output config/parse_config_test.go ParseConfig config
|
||||
bash $SCRIPT_NAME --json --output config/parse_config_test.go ParseConfig config
|
||||
bash $SCRIPT_NAME ParseConfig config > config/parse_config_test.go
|
||||
EOF
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
OUTPUT=""
|
||||
PARALLEL=false
|
||||
JSON_OUTPUT=false
|
||||
FORCE=false
|
||||
POSITIONAL=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
--output) OUTPUT="${2:?error: --output requires a file path}"; shift 2 ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--parallel) PARALLEL=true; shift ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) POSITIONAL+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#POSITIONAL[@]} -lt 2 ]]; then
|
||||
echo "error: FuncName and package are required" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
FUNC="${POSITIONAL[0]}"
|
||||
PKG="${POSITIONAL[1]}"
|
||||
|
||||
if [[ ! "$FUNC" =~ ^[A-Z] ]]; then
|
||||
echo "error: FuncName '$FUNC' must start with an uppercase letter" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
generate_test() {
|
||||
local parallel_top="" parallel_sub=""
|
||||
if $PARALLEL; then
|
||||
parallel_top=$'\tt.Parallel()\n'
|
||||
parallel_sub=$'\t\t\tt.Parallel()\n'
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
package ${PKG}
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test${FUNC}(t *testing.T) {
|
||||
${parallel_top} tests := []struct {
|
||||
name string
|
||||
give string // TODO: replace with actual input type
|
||||
want string // TODO: replace with actual output type
|
||||
}{
|
||||
{
|
||||
name: "basic case",
|
||||
give: "",
|
||||
want: "",
|
||||
},
|
||||
// TODO: add more test cases
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
${parallel_sub} got := ${FUNC}(tt.give)
|
||||
if got != tt.want {
|
||||
t.Errorf("${FUNC}(%q) = %q, want %q", tt.give, got, tt.want)
|
||||
}
|
||||
// For richer diffs, consider:
|
||||
// if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
// t.Errorf("${FUNC}() mismatch (-want +got):\n%s", diff)
|
||||
// }
|
||||
})
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ -n "$OUTPUT" ]]; then
|
||||
OUTPUT_DIR="$(dirname "$OUTPUT")"
|
||||
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||
echo "error: directory '$OUTPUT_DIR' does not exist" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -f "$OUTPUT" ]] && ! $FORCE; then
|
||||
echo "error: '$OUTPUT' already exists (use --force to overwrite)" >&2
|
||||
exit 2
|
||||
fi
|
||||
generate_test > "$OUTPUT"
|
||||
if $JSON_OUTPUT; then
|
||||
FUNC_ESC="$(json_escape "$FUNC")"
|
||||
PKG_ESC="$(json_escape "$PKG")"
|
||||
OUTPUT_ESC="$(json_escape "$OUTPUT")"
|
||||
cat <<EOF
|
||||
{"func":"$FUNC_ESC","package":"$PKG_ESC","output_file":"$OUTPUT_ESC","parallel":$PARALLEL,"written":true}
|
||||
EOF
|
||||
else
|
||||
echo "Wrote test scaffold to $OUTPUT"
|
||||
fi
|
||||
else
|
||||
if $JSON_OUTPUT; then
|
||||
generate_test >&2
|
||||
FUNC_ESC="$(json_escape "$FUNC")"
|
||||
PKG_ESC="$(json_escape "$PKG")"
|
||||
cat <<EOF
|
||||
{"func":"$FUNC_ESC","package":"$PKG_ESC","output_file":"","parallel":$PARALLEL,"written":false}
|
||||
EOF
|
||||
else
|
||||
generate_test
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user