1、测试哲学:为什么需要模拟边界?领域驱动设计中的测试金字塔与六边形架构
在领域驱动设计(DDD)与六边形架构(Hexagonal Architecture)的实践中,测试的核心挑战并非“如何写代码”,而是“如何隔离领域逻辑与外部依赖”。模拟边界(Mock & Stub)正是为了解决这一隔离问题而诞生的测试哲学。
1.1 测试金字塔:从底层到顶层的投入策略
测试金字塔由 Mike Cohn 提出,指导团队将测试资源按层级分配。在 DDD 项目中,金字塔的每一层对应不同的验证目标:
| 层级 | 验证对象 | 典型工具 | 运行速度 | 维护成本 |
|---|---|---|---|---|
| 单元测试 | 领域实体、值对象、领域服务 | Go 原生 testing + testify | 毫秒级 | 低 |
| 集成测试 | 仓储实现、消息适配器、外部 API 客户端 | testcontainers-go / httptest | 秒级 | 中 |
| 端到端测试 | 完整用户旅程、系统边界交互 | cucumber / godog | 分钟级 | 高 |
关键原则:单元测试应占总测试量的 70% 以上。领域逻辑的每一次分支、每一个业务规则,都应在不启动数据库、不发送真实 HTTP 请求的前提下完成验证。
1.2 六边形架构:领域是核心,边界是端口
六边形架构(又称端口与适配器模式)将应用程序分为三层:
- 领域层(内六边形):纯业务逻辑,零外部依赖。
- 应用层:编排用例,定义输入/输出端口(接口)。
- 基础设施层(外六边形):实现端口,连接数据库、消息队列、外部 API。
这种架构天然要求领域层不感知任何具体实现——它只依赖接口。而测试中的 Mock 与 Stub,正是这些接口的“替身演员”。
1.3 为什么必须模拟边界?三大核心原因
- 速度:一次真实的数据库查询可能耗时 10~50ms,而一个 Stub 返回结果仅需 0.001ms。当测试数量达到数千个时,速度差异决定开发体验。
- 确定性:外部服务可能超时、返回错误、或数据不一致。Mock 让你在测试中精确控制每种场景(成功、失败、超时、空结果),无需依赖外部环境。
- 领域隔离:DDD 要求领域逻辑独立于技术实现。如果不模拟边界,测试会同时验证领域规则和基础设施行为,导致失败时无法快速定位问题归属。
常见误区:不要对领域实体本身使用 Mock。实体是纯数据+行为,应直接构造真实实例进行测试。Mock 只应用于领域层与外部世界的接口——即六边形架构中的端口。
1.4 Mock 与 Stub 的本质区别
| 特性 | Stub(桩) | Mock(模拟) |
|---|---|---|
| 核心目的 | 提供预设返回值 | 验证交互行为 |
| 是否验证调用次数 | 否 | 是 |
| 典型场景 | 仓储查询返回固定实体 | 验证领域服务是否调用了发送通知接口 |
| Go 实现方式 | 结构体实现接口 + 硬编码返回 | testify/mock 或 gomock 生成 |
1.5 在 Go 项目中的实践原则
- 面向接口编程:领域层定义端口(interface),基础设施层实现它。测试时注入 Stub 或 Mock。
- 依赖注入:通过构造函数或选项模式将依赖传入领域服务,避免在领域层内部直接 new 具体实现。
- 测试替身只用于边界:不要 Mock 标准库、不要 Mock 领域实体、不要 Mock 你未拥有的代码。