CI/CD流水线中的Mock策略:在构建、测试、部署阶段如何嵌入Mock
说实话,很多人把 Mock 当成测试阶段的“临时工”——测试跑完了就扔。但在我参与的几个大型微服务项目里,Mock 要是没在流水线里好好“安家”,后面全是坑。今天我就聊聊在 CI/CD 的三个核心阶段:构建、测试、部署,怎么把 Mock 嵌进去,让它成为流水线的稳定器,而不是绊脚石。
一、为什么要分阶段嵌入?一句话:风险隔离
想象一下:开发提交了代码,构建阶段就开始调真实服务——网络抖动一下,整个流水线红了。其实没必要。Mock 在不同阶段扮演的角色不同:
- 构建阶段: 快速验证代码能否正常编译,依赖的服务接口是否匹配。
- 测试阶段: 单元测试用轻量 Mock,集成测试用契约 Mock,回归测试用录制回放。
- 部署阶段: 在预发环境里用 Mock 模拟下游异常、超时等场景,验证容错逻辑。
核心原则:越靠近代码提交,Mock 越轻量;越靠近生产,Mock 越接近真实契约。
我在一个电商项目中见过最典型的反例:测试环境里所有外部服务都 Mock 死了,结果上线后真实支付服务返回了一个意料之外的错误码,系统直接崩溃。嗯,从那以后我坚持:部署阶段的 Mock 必须有“负向测试”覆盖。
二、构建阶段:死得快,Mock 也得快
构建阶段的目标是快速反馈——代码有没有编译错误、接口签名是否对得上。这时候我用的是轻量桩代码(Stub),而不是完整的 Mock 框架。
举个例子,你的服务依赖一个用户服务接口 GET /api/user/{id},构建时不需要真的去调用户服务,只需一个“哑实现”返回固定 JSON:
// 构建阶段使用的 Stub(Java 示例)
public class UserServiceStub implements UserService {
@Override
public User getUser(String id) {
// 只校验参数,不访问网络
if (id == null) {
throw new IllegalArgumentException("id 不能为空");
}
return new User("mock-id", "mock-name", "mock@email.com");
}
}
这么做的好处就一个:快。我习惯把这类桩代码放在 src/test/stubs 目录下,用 Maven profile 或 Gradle build variant 控制开关。构建 CI 脚本里加一行:
# Jenkinsfile 片段
stage('Build with Stubs') {
steps {
sh './gradlew build -PuseStubs=true'
}
}
我曾经踩过坑:Stub 只返回成功数据,导致代码里一些判空逻辑根本没走到。后来我加了几个“异常 Stub”放在 stubs/errors 里,构建时随机选一个跑,反正编译能过就行。这是个低成本但是高回报的小技巧。
三、测试阶段:三道“安检门”
测试阶段是最需要仔细设计 Mock 策略的地方。我把测试拆成三层,每层用不同的 Mock 手段:
| 测试层次 | Mock 策略 | 主要目的 |
|---|---|---|
| 单元测试 | Mockito / Moq(内存内模拟) | 隔离内部逻辑,快速验证 |
| 集成测试 | 契约 Mock(Pact / WireMock) | 验证接口契约是否一致 |
| 回归测试 | 录制回放(例如 VCR / Hoverfly) | 保证旧逻辑不被新代码破坏 |
为什么会这样分层?说白了,单元测试要的是“准”,集成测试要的是“对”,回归测试要的是“稳”。我建议流水线里这样顺序执行:
- 先跑单元测试(用 Mockito),失败则直接退出。
- 再跑集成测试(用 WireMock 模拟外部服务),失败则检查契约。
- 最后跑回归测试(用录制回放),失败则大概率是新代码破坏了原有场景。
注意:录制回放里的“回放”并不是完美不变的。我在项目中遇到过录制版本和真实服务版本不一致,导致回放通过但上线后接口报错。所以建议集成测试阶段同时跑一轮“真实服务验证”——哪怕只跑一个关键场景。
四、部署阶段:Mock 是“防火墙”
部署阶段,尤其是灰度发布或蓝绿部署时,我要的不是“跑得快”,而是“跑得稳”。这时候我用 Mock 来模拟各种异常边界:
- 下游服务超时(响应延迟 10 秒以上)
- 下游返回 500 错误
- 下游返回意料之外的响应体(比如 JSON 里多了一个字段)
- 网络分区(完全断连)
做法很简单:在预发环境部署一个 Mock Proxy(比如 Hoverfly 或 Mountebank),通过环境变量控制流量是否经过 Mock。CI 脚本大概是这样的:
# 部署到预发环境,并启用 Mock Proxy
export MOCK_PROXY_ENABLED=true
export MOCK_PROXY_URL=http://mock-proxy.internal:8500
docker-compose -f docker-compose.preprod.yml up -d
# 运行冒烟测试,覆盖异常场景
./run-smoke-tests.sh --scenario=timeout,error,malformed
一个小窍门:冒烟测试的断言不要只看 HTTP 200,可以检查业务指标——比如错误率是否在预期范围内、降级逻辑是否触发。我吃过一次亏:Mock 返回了 200,但返回值是空对象,业务日志里一堆 NPE,流水线没发现。
五、整个流水线的 Mock 结构图
下面这张图把三个阶段以及对应的 Mock 策略串了起来,方便你对照着看:
六、几个实战避坑指南
避坑 1:我曾经在构建阶段把 Stub 不小心放到了生产编译路径里,结果打成 jar 包后,线上业务代码里嵌入了 Mock 数据。解决方法:用 sourceSets 严格隔离,CI 里跑一次 grep -r "mock" src/main/ 做“防意外”检查。
避坑 2:测试阶段的 WireMock 录制文件(.json 或 .yaml)如果多人同时修改,容易冲突。我建议把录制文件也纳入版本控制,并在 PR 触发 CI 时,增加一个“契约校验”步骤——如果录制文件与真实服务接口不一致,流水线直接失败。
避坑 3:部署阶段的 Mock Proxy 如果一直开着,线上真实流量过来就乱了。我习惯在 CI 脚本里显式销毁 Mock Proxy 容器:docker rm -f mock-proxy 放在部署完成后的清理步骤里。千万别忘,不然预发环境会变成“假环境”。
最后总结一句话:Mock 不是用来偷懒的,是用来管理不确定性的。在流水线里把 Mock 策略分布到构建、测试、部署三个阶段,就像给系统装了三级保险——每过一关,风险就降一截。希望今天聊的这些经验,能帮你少踩几个坑。