mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 14:46:36 +08:00
168 lines
4.4 KiB
Markdown
168 lines
4.4 KiB
Markdown
# 测试组织参考
|
|
|
|
来源: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))。
|