Table of contents
Open Table of contents
TL;DR
Clean Architecture 做对了的话,大部分层根本不需要 mock:domain 层零依赖直接测,repository 层必须用真实基础设施(testcontainers),use case 层视编排复杂度决定。Mock 只在外部不可控系统(支付网关、短信)和故障注入场景下合理。
分层看测试策略
| 层 | 需不需要 mock? | 为什么 |
|---|---|---|
| Domain | 不需要,也没东西可 mock | 零依赖,纯函数/方法,直接调直接断言 |
| Use Case | 视复杂度而定 | 简单编排(validate → save)→ 直接集成测试;复杂编排(条件分支、多 repo 协调、补偿逻辑)→ mock repo 测所有路径更高效 |
| Repository/Adapter | 必须用真实基础设施 | 这里 mock 是最有害的——你测的是 SQL 对不对、mapping 对不对,mock 掉就等于没测 |
| API 端到端 | 真实请求 + 真实 DB | testcontainers 起 PostgreSQL,发真实 HTTP/gRPC 请求 |
为什么 mock 在 repository 层有害
// 用 mock 测试——永远不会失败,但生产环境 SQL 可能是错的
mockRepo.On("GetByID", 1).Return(&domain.User{Name: "test"}, nil)
// 用 testcontainers——SQL 真的跑了一遍,schema 不对立刻爆
func TestGetByID(t *testing.T) {
ctx := context.Background()
pg, _ := postgres.Run(ctx, "postgres:16",
postgres.WithDatabase("test"),
postgres.WithInitScripts("schema.sql"),
)
defer pg.Terminate(ctx)
db := connectTo(pg)
repo := NewPostgresUserRepo(gen.New(db))
user, err := repo.GetByID(1)
// 这个测试真的验证了 SQL + mapping
}
Mock 测的是你对数据库行为的假设,不是数据库的真实行为。当假设和现实偏离(schema 迁移、字段类型变更、NULL 处理),mock 测试照样绿,生产炸了。
Clean Architecture 让 no-mock 更容易
反直觉的点:分层不是为了”每层都 mock”,而是为了让某些层不需要任何 test double。
Domain 测试: user.Validate() → 纯逻辑,不需要任何依赖
UseCase 测试: 注入真实 repo(testcontainers)→ 集成测试
API 测试: 真实 HTTP 请求 → 端到端测试
如果没有 Clean Architecture,业务逻辑和 SQL 混在 handler 里,你要么 mock 数据库测业务逻辑,要么每个测试都起数据库——前者不靠谱,后者太慢。分层之后,domain 测试不需要数据库,集成测试才需要。
什么时候 mock 仍然合理
- 第三方外部 API(支付网关、短信服务):你不能每次测试都真的扣钱发短信
- 复杂编排的路径覆盖:一个 use case 有 5 个分支,用 mock 测 5 条路径比起 5 次数据库 setup 快得多
- 故障注入:测试”数据库超时了怎么办”——真实数据库不容易模拟超时
原则:mock 外部系统的不可控行为,不 mock 你自己的代码。