Java 项目中的单元测试、集成测试和端到端测试如何划分?
简化版
三类测试按范围和成本分层:单元测试验证一个小单元(类/方法)的逻辑,用 mock 隔离依赖,快、稳、定位准;集成测试验证多个组件 + 真实基础设施(数据库、缓存、MQ、Spring 容器)的协作,能发现配置、SQL、事务、序列化等 mock 测不出的问题,慢一些;端到端(E2E)测试从用户/接口入口验证完整链路(登录→下单→支付),最真实但最慢、最脆弱。测试金字塔思想:大量单元测试兜住细节、适量集成测试兜住协作、少量 E2E 兜住关键主流程,兼顾反馈速度和上线信心。
详细版
三类测试对比:
| 维度 | 单元测试 | 集成测试 | 端到端测试(E2E) |
|---|---|---|---|
| 范围 | 一个类/方法 | 多个组件 + 真实设施 | 完整业务链路 |
| 依赖 | mock/fake 替换 | 真实 DB/缓存/MQ/Spring | 真实部署环境 |
| 速度 | 极快(毫秒) | 较慢(秒) | 慢(秒~分钟) |
| 稳定性 | 高 | 中 | 低(环境敏感) |
| 定位精度 | 高(直接指出哪段逻辑) | 中 | 低(链路长) |
| 数量 | 多(金字塔底) | 适量(中层) | 少(塔尖) |
测试金字塔:
/\ E2E(少)—— 关键主流程
/ \
/----\ 集成测试(适量)—— 组件协作
/------\
/--------\ 单元测试(大量)—— 业务逻辑细节
Java 项目的典型落地:
- 单元测试:JUnit + Mockito,测业务 Service 类。
- 集成测试:Testcontainers 测 Repository/SQL/消息;
@WebMvcTest+ MockMvc 测 Controller 层。 - E2E:少量真实环境跑关键链路。
完整版教学
一、分层的根本原因
三类测试不是「谁替代谁」,而是在速度、信心、维护成本之间做平衡:
- 测试越接近底层(单元):反馈越快、定位越准(失败了直接知道哪段逻辑错),但覆盖的链路窄(测不出组件协作问题)。
- 测试越接近真实用户(E2E):覆盖链路越完整、越有上线信心,但越慢、越容易受环境影响(脆弱)。
分层的意义就是:用大量快而准的单元测试兜住细节,用少量真实的 E2E 兜住关键路径,取两者所长。这不是「仪式感」,而是工程上的成本-收益权衡。
| 层级 | 反馈速度 | 定位精度 | 真实程度 | 推荐数量 |
|---|---|---|---|---|
| 单元测试 | 毫秒级 | 高 | 低到中 | 多 |
| 集成测试 | 秒级 | 中 | 中到高 | 适量 |
| E2E | 秒到分钟级 | 低 | 最高 | 少 |
测试金字塔不是口号,而是用不同成本的测试覆盖不同风险:细节靠单元,协作靠集成,主流程靠 E2E。
二、单元测试看什么
单元测试关注「一个类/函数在给定输入下是否产生正确输出」。典型的被测对象是纯业务逻辑:
- 金额计算、税率、折扣。
- 状态流转(订单状态机)、权限判断、参数校验、策略选择。
它应该不依赖网络和真实数据库(用 mock 替换 DAO、远程客户端)。好的单元测试运行毫秒级、稳定、失败时能立刻指出是哪段业务逻辑错了。这是测试金字塔的地基,数量最多。
三、集成测试看什么
集成测试关注「组件之间是否真的能配合」——验证那些mock 发现不了、只有真实环境才暴露的问题:
- MyBatis/JPA 的 SQL 是否正确(字段名、语法、映射)。
- 事务是否按预期回滚。
- JSON 序列化字段是否兼容(生产者写的能被消费者读懂)。
- Spring Bean 是否装配成功、配置绑定是否正确。
- 消息序列化能否被消费者读懂。
这些问题用 mock 测不出来(mock 假设一切正常),必须连真实的数据库、缓存、消息中间件、Spring 容器才能发现。所以集成测试慢一些,但价值不可替代。
四、端到端测试看什么
E2E 从真实入口验证完整流程——例如「用户登录 → 下单 → 支付回调 → 订单状态变更」整条链路。它最能代表用户视角、最有上线信心。
但代价是:慢、脆弱——链路长,任何一环的环境抖动(网络、依赖服务、数据)都可能导致 E2E 失败(且失败了不好定位是哪一环)。所以 E2E 只适合覆盖少量高价值的关键路径(核心交易主流程),不该用它覆盖所有细节。
五、常见反模式
- 把所有测试都写成
@SpringBootTest:每个测试都启动完整 Spring 上下文,慢到没人愿意跑——失去了单元测试快速反馈的意义。 - 只写 mock 单元测试:会漏掉配置、SQL、序列化、装配等真实协作问题(这些 mock 发现不了)。
- 大量 E2E 覆盖细节:让 CI 变得又慢又脆弱,动不动就红,团队逐渐失去对测试的信任。
好的测试组合是各层各司其职:细节交给单元、协作交给集成、主流程交给 E2E,而不是用一层去干所有事。
E2E: 登录 -> 下单 -> 支付 -> 回调
集成: Repository + PostgreSQL / WebMvc + JSON
单元: 金额计算 / 状态流转 / 参数校验
六、Java 项目中的落地方式
具体到 Java/Spring 项目:
- 业务 Service 类 → JUnit + Mockito 写单元测试(mock 掉 DAO 和外部依赖)。
- Repository / 消息组件 → Testcontainers 做集成测试(用真实的 Docker 化数据库/中间件,见 Testcontainers 专题)。
- Controller 层 →
@WebMvcTest+ MockMvc(或响应式的 WebTestClient)验证 HTTP 层(参数绑定、状态码、JSON)——这是「切片测试」,只加载 Web 层,比全量@SpringBootTest快。 - 关键业务链路 → 少量 真实部署环境的 E2E。
七、如何判断测试质量
测试质量不看数量、不唯覆盖率,看几个实质指标:
- 失败是否容易定位(能否快速指出哪里错)。
- 是否稳定(不会随机红/绿——flaky 测试比没测试还糟,会让人忽视真实失败)。
- 是否覆盖了重要风险(核心业务逻辑、边界、异常分支)。
- 是否支持重构(重构内部实现、业务不变时测试应继续通过)。
覆盖率只是「观察指标」,不是「最终目的」——没有断言、只为覆盖行数存在的测试没有保护价值(见「测试覆盖率」专题)。宁可少而精,不要多而空。
八、常见误区与追问
- 误区:测试越接近真实环境越应该多写。 越真实通常越慢越脆弱,E2E 应覆盖关键主流程,而不是所有细节。
- 误区:单元测试用了 mock 就没有价值。 mock 隔离外部依赖后,单元测试能快速稳定地验证业务规则和边界。
- 误区:集成测试只是启动 Spring 容器。 它重点验证组件协作和真实设施行为,如 SQL、事务、序列化、配置装配。
- 追问:为什么大量
@SpringBootTest是反模式? 全量启动上下文慢,反馈差,容易把所有测试都变成沉重的集成测试。 - 追问:E2E 失败为什么定位难? 链路长,任何服务、网络、数据、环境问题都可能导致失败,需要更多排障成本。
- 追问:如何判断一条测试该放在哪一层? 看它要验证的风险:纯业务规则放单元,真实依赖协作放集成,用户关键路径放 E2E。
九、加强记忆
三类测试按范围分层:单元测试(一个类/方法逻辑,mock 隔离依赖,快/稳/定位准,测金额/状态流转/校验,数量最多)、集成测试(多组件 + 真实 DB/缓存/MQ/Spring 协作,发现 SQL/事务/序列化/装配 等 mock 测不出的问题,用 Testcontainers/@WebMvcTest,适量)、E2E(真实入口的完整链路如登录→下单→支付,最真实但最慢最脆弱,只覆盖少量关键主流程)。测试金字塔:大量单元 + 适量集成 + 少量 E2E,兼顾反馈速度和上线信心。反模式:全用 @SpringBootTest(慢没人跑)、只写 mock(漏协作问题)、大量 E2E(CI 脆弱)。覆盖率是观察指标不是目的,无断言的测试没价值。