144 lines
5.1 KiB
Markdown
144 lines
5.1 KiB
Markdown
---
|
||
tags: [go, testing, integration-test, project-structure, engineering]
|
||
create time: 2026-04-29 15:45
|
||
---
|
||
|
||
# 集成测试目录结构
|
||
|
||
## 概述
|
||
|
||
梳理 Go 项目中测试目录的组织方式,聚焦集成测试的定位、目录划分和最佳实践。明确单测与集成测试的边界,帮助团队在工程规模增长时保持测试可维护性。
|
||
|
||
## 为什么需要独立测试目录?
|
||
|
||
> [!question] 思考:单测不够吗?为什么还要单独建 tests/?
|
||
|
||
Go 的社区惯例是**单测和源码放一起**(`xxx_test.go`),这已经覆盖了绝大多数场景。但当测试需要真实基础设施(数据库、Redis、HTTP Server)时,就需要引入独立的集成测试目录。
|
||
|
||
| 维度 | 包内单测 | `tests/` 集成测试 |
|
||
|------|---------|-------------------|
|
||
| **视角** | 白盒——能看到 unexported | 黑盒——只调公开 API |
|
||
| **速度** | 快(纯内存操作) | 慢(需启动依赖) |
|
||
| **职责** | 验证函数行为 | 验证组件协作 |
|
||
| **CI 触发** | 每次 commit 必跑 | 可单独控制,按需跑 |
|
||
|
||
## tests/ 的典型结构
|
||
|
||
```
|
||
tests/
|
||
├── integration/ # 集成测试:组件之间正确协作
|
||
│ ├── user_test.go # 用户注册全流程(handler → service → repo → DB)
|
||
│ └── order_test.go # 下单流程(含事务、并发安全校验)
|
||
├── e2e/ # 端到端测试:整个服务对外可见的行为
|
||
│ └── api_smoke_test.go # 启动 server,发 HTTP 请求做冒烟测试
|
||
├── fixtures/ # 测试数据fixtures
|
||
│ ├── users.json # 预置数据
|
||
│ └── orders.json
|
||
└── testutil/ # 集成测试专用辅助包
|
||
└── db.go # TestDB() 封装:创建测试库 + 自动清理
|
||
```
|
||
|
||
### 各子目录的职责
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["tests/"] --> B["integration/ — 集成测试<br/>组件协作 + 真实依赖"]
|
||
A --> C["e2e/ — 端到端测试<br/>完整服务 + HTTP 调用"]
|
||
A --> D["fixtures/ — 测试数据"]
|
||
A --> E["testutil/ — 基础设施辅助"]
|
||
|
||
style B fill:#bfb,stroke:#333,stroke-width:3px
|
||
style C fill:#fbf,stroke:#333
|
||
style D fill:#fff,stroke:#333
|
||
style E fill:#fff,stroke:#333
|
||
```
|
||
|
||
> [!tip] 何时需要哪个?
|
||
>
|
||
> - **只有 unit test**:项目初期、CRUD 为主 → 不需要 `tests/`,全放包内
|
||
> - **加了外部依赖后**:有数据库/缓存/消息队列 → 引入 `tests/integration/`
|
||
> - **上线前质量保障**:微服务、对外 API → 增加 `tests/e2e/`
|
||
|
||
## 集成测试的最佳实践
|
||
|
||
### 1. 测试数据隔离
|
||
|
||
每个测试用例应使用独立的数据命名空间,避免相互污染:
|
||
|
||
```go
|
||
func TestCreateOrder(t *testing.T) {
|
||
// 为当前测试准备干净的数据库状态
|
||
db := testutil.NewTestDB(t)
|
||
defer db.Cleanup() // 事务回滚或 truncate
|
||
|
||
svc := service.NewOrderService(db.Conn())
|
||
res, err := svc.Create(ctx, payload)
|
||
assert.NoError(t, err)
|
||
|
||
// 断言基于干净状态的预期结果
|
||
}
|
||
```
|
||
|
||
> [!info] 两种隔离策略
|
||
|
||
| 策略 | 做法 | 适用场景 |
|
||
|------|------|----------|
|
||
| **事务回滚** | 每件事务包裹在 BEGIN/ROLLBACK 中 | PostgreSQL/MySQL,速度快 |
|
||
| **临时 Schema** | 每个 test 创建独立 schema | 需要测试迁移、DDL 等场景 |
|
||
|
||
### 2. 避免硬编码端口
|
||
|
||
集成测试需要启动服务,端口冲突是 CI 最常见的失败原因:
|
||
|
||
```go
|
||
func TestAPIIntegration(t *testing.T) {
|
||
// ✅ 动态分配可用端口
|
||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||
addr := l.Addr().String()
|
||
l.Close()
|
||
|
||
srv := &http.Server{Addr: addr, Handler: router}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
> [!warning] 常见坑
|
||
>
|
||
> - 不要写死 `8080`——本地开发可能已经在跑别的服务
|
||
> - `net.Listener` 用 `"127.0.0.1:0"` 让内核分配空闲端口是最稳妥的方式
|
||
|
||
### 3. CI 中的分级执行
|
||
|
||
```yaml
|
||
# .github/workflows/test.yml 示例思路
|
||
- run: go test ./... # 所有单测(每次 commit)
|
||
- run: go test ./tests/integration/... # 集成测试(push to main 或手动触发)
|
||
- run: go test ./tests/e2e/... # E2E(release 前)
|
||
```
|
||
|
||
> [!summary] 分级执行的理由
|
||
>
|
||
> 集成测试和 E2E 跑起来可能几分钟甚至更久,混在常规 CI 里会拖慢开发节奏。按阶段分级,既能保证质量又不牺牲效率。
|
||
|
||
## 与内部包的配合
|
||
|
||
集成测试虽然放在项目根级的 `tests/` 下,但它访问的是你 `internal/` 里的代码。这里有个重要细节:
|
||
|
||
```
|
||
my-project/
|
||
├── internal/user/service/
|
||
│ └── service.go # package service — 对外不可见
|
||
├── tests/integration/
|
||
│ └── user_test.go # import "my-project/internal/user/service" — 合法!
|
||
```
|
||
|
||
> [!tip] 关键点
|
||
>
|
||
> - `internal/` 的限制是**对其它 module 而言**的,同一 module 内的任何位置都可以 import
|
||
> - `tests/` 和 `internal/` 属于同一个 module,所以可以毫无阻碍地访问私有包
|
||
> - 这意味着集成测试其实是**半白盒**的——你能看到 internal 但看不到第三方 module 的代码
|
||
|
||
## 关联笔记
|
||
|
||
- [[Go 工程模块化]]
|