后端服务如何做单元测试 mock 外部依赖?可能类似单元测试。
结论:后端单元测试 mock 外部依赖的正确原则是「mock 端口,不 mock 实现」——把 HTTP 调用、数据库、缓存、消息队列、时钟这些跨进程且不可控的 IO,封装在自定义接口(端口)后面,测试时通过依赖注入换成内存假实现或本地 fake server,被测的业务代码一行都不改。
哪些依赖该 mock,哪些不该 mock?
结论:只有「跨进程 + 不可控 + 慢」的依赖才需要替身,进程内的值对象、DTO、领域服务必须用真实实现。
必须替换的清单:第三方 HTTP API、数据库、Redis、MQ(Kafka/RabbitMQ)、短信与推送网关、对象存储、System.currentTimeMillis() 这类时间源、随机数与 UUID 生成器。
不该 mock 的:你自己写的 User、Money、OrderPolicy 这类纯内存对象和纯计算逻辑。判断标准很直接:如果 Mockito 里 mock() 的类型是你自己写的业务类,说明分层已经错了,测试只是在验证「调用顺序」而不是业务结果。
HTTP 外部调用怎么 mock?
结论:在 HTTP 协议层拦截,用本地 fake server 跑真实的序列化流程,而不是 mock 你自己封装的客户端方法。
- Java:WireMock,JUnit 5 下用
@WireMockTest,写stubFor(get(urlEqualTo("/v1/user/1")).willReturn(aResponse().withStatus(200).withHeader("Content-Type","application/json").withBody(json))) - Python:
responses或requests-mock;用 httpx 的话用httpx.MockTransport - Go:标准库
net/http/httptest.NewServer,把srv.URL注入配置
这样 JSON 反序列化、字段缺失、404/500 分支、超时重试全都被真实覆盖。如果你 mock 的是 userClient.getUser() 这个方法,上面这些全测不到。
数据库和缓存要不要 mock?
结论:Repository/DAO 层用 Testcontainers 起真实数据库,Service 层用接口替身。
H2 的 MySQL 兼容模式在 JSON_EXTRACT、ON DUPLICATE KEY UPDATE、INSERT IGNORE 上行为不一致,用 H2 替代 MySQL 8 会在 CI 上出现「本地过、流水线红」。Testcontainers 起 MySQL 8 容器冷启动 5–15 秒,加上 withReuse(true) 复用后单次可降到 1 秒内。
Service 层的单测则把 UserRepository 换成内存实现,一个 Map<Long, User> 就够,测试执行时间在毫秒级。
在 Spring Boot / JUnit 5 里具体怎么写?
结论:用构造函数注入端口接口,测试里手动传入 Fake,能不启动 Spring 上下文就不启动。
- 定义端口:
interface UserClient { User get(long id); } - 生产实现
HttpUserClient,内部用 RestTemplate/WebClient - 测试实现
FakeUserClient implements UserClient,内部Map<Long,User> UserService构造函数接收UserClient- 测试写
new UserService(new FakeUserClient(fixture)),不用@SpringBootTest
时间依赖同理:注入 java.time.Clock,测试用 Clock.fixed(Instant.parse("2024-01-01T00:00:00Z"), ZoneOffset.UTC)。Python 侧用 freezegun 的 @freeze_time("2024-01-01")。
怎么保证 mock 不漂移?
结论:用消费者驱动的契约测试兜底,Pact 或 Spring Cloud Contract 都可以。
消费者测试生成 pact 文件,提供者在 CI 里回放验证;提供者改了字段名或状态码,消费者流水线立刻失败。没有契约测试时,你 mock 出来的响应和真实服务会随着时间悄悄分叉,这是「单测全绿但线上 500」的最常见成因。
工程阈值参考:行覆盖率 70%–80%,单个测试方法内 stub 不超过 5 个,超出说明这个用例该拆。
常见的 mock 反模式有哪些?
结论:四类写法一定会在重构时爆炸。
- 链式 mock:
when(a.getB().getC()).thenReturn(d),中间对象一加包装层,全组测试红 - mock 自己写的 Service,测试只验证了「调没调」,没验证任何计算
- 只 stub 成功路径,不 stub 超时和 500,重试与降级代码零覆盖
- 静态 mock 不清理,测试之间互相污染,单独跑绿、全量跑红
收束一下:先按进程边界切端口,HTTP 用 WireMock/httptest,数据库用 Testcontainers,Service 层用内存 Fake,时间用 Clock 注入,最后用契约测试锁住 mock 与真实服务的一致性。做到这五步,单测才真正在你重构时提供保护,而不是变成必须同步修改的负担。
原文链接:https://www.gj0.com/thread-804.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。