--- 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/ — 集成测试
组件协作 + 真实依赖"] A --> C["e2e/ — 端到端测试
完整服务 + 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 工程模块化]]