读完这篇,你能做什么
能为同一功能选择单元、切片与集成测试,并准确报告各自证明范围。
学习理由、准备要求与知识点
为什么学它
测试范围必须与要证明的行为相符,才能避免用模拟通过冒充真实链路验证。
先读这些内容
知识点清单
- 行为与边界断言
- 普通单元测试
WebMvcTest切片MockMvc模拟请求SpringBootTest范围- 真实数据库与方言
- 测试数据和异步事务
- 执行数量与报告证据
运行前先确认Java17、Spring Boot3.4、JUnit5;MockMvc源码固定Spring Framework v6.2.11。算法是离线测试覆盖规划,不调用真实Spring、数据库或网络,不等同于集成验证。
01先弄懂它是什么
选测试工具之前,先写清要证明什么。新增菜品可能分别需要检查价格规则、JSON 校验、数据库唯一约束和反向代理路径,这四项不是同一个范围。纯 Java 单元测试能直接创建 Service,用替身提供依赖来检查业务结果;替身答应写入成功,并不能证明真实 SQL 写对了。
@WebMvcTest 配合 MockMvc 通常只加载 MVC 相关部分,检查 Controller、参数读取、校验和错误响应,业务依赖可能需要显式补充或替换。MockMvc 模拟 Servlet 请求处理,不启动真实网络端口。因此这类测试可以证明 MVC 行为,却不能证明 TLS、监听地址或反向代理转发正常。
@SpringBootTest加载应用上下文,但是否启动网络服务器取决于webEnvironment;依赖是否真实运行也由配置决定。默认模拟环境与RANDOM_PORT真实服务器不是同一范围。数据库集成测试还需明确数据库产品版本、迁移脚本、数据隔离和清理策略;内存数据库通过不能直接外推到MySQL方言与锁行为。
文中这几个词,先认一认
- 测试替身
- 测试中代替真实依赖的对象;它按设定返回结果,只能证明这些模拟条件下的行为。
- 切片测试
- 只加载某一层需要的框架组件,用较小范围检查这一层是否按约定工作。
- 证据范围
- 测试实际运行了哪些代码、配置和外部依赖,结论不能超出这个范围。
02看看它怎么工作
从一个明确HTTP契约开始:GET /api/ping应返回200和文本pong,非法请求应具有约定状态与错误体。MockMvc的perform构造请求后,执行过滤链与测试Servlet,再交给andExpect断言。断言要同时覆盖状态、内容和必要头部,只有isOk可能放过返回结构错误或业务空结果。
测试方法里的事务通常跟着执行它的线程。若另开后台线程,或通过 RANDOM_PORT 请求真实服务器,写数据库的代码可能在另一条线程上运行,不会自动加入测试线程的事务。因此测试方法最后回滚,不代表服务器刚写的数据也被撤销;要使用隔离库、能识别的测试数据或明确清理。某些抢占式超时工具也会换线程执行被测代码,使用前同样要检查事务是否还在预期范围。
测试报告应能解释执行情况。mvn test后查看实际测试数量、失败和跳过状态;集成测试可能由其他插件在verify阶段运行,不能只凭BUILD SUCCESS推断全部已覆盖。对异步行为用有界等待观察目标状态,避免固定sleep既慢又不稳定,也不要无限轮询把故障变成挂起。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["待证明行为"] --> B{"主要边界"}
B --> C["纯业务单元测试"]
B --> D["MVC切片"]
B --> E["数据库集成测试"]
B --> F["真实HTTP验收"]
C --> G["结果与边界断言"]
D --> G
E --> G
F --> G
G --> H["报告实际覆盖与未覆盖"]按顺序看,每一步做了什么
- 写出成功、非法输入、未授权和异常路径的期望。
- 为每条期望选最小但足够的执行范围。
- 明确哪些协作者真实执行,哪些使用替身。
- 安排数据隔离、异步等待与清理,不依赖测试顺序。
- 检查执行数量、报告与目标环境,限定测试结论。
03对照实现代码
Spring Framework v6.2.11 · MockMvc#perform(RequestBuilder)
原创教学实现,非源码节选。不实现Servlet或Spring,只保留请求构造、处理与结果断言的分离;真实MockMvc还处理过滤器、请求上下文与异步分派。
import java.util.function.*;
class SourceModel {
record Request(String path) {}
record Response(int status,String body) {}
static Response perform(Supplier<Request> builder,
Function<Request,Response> handler, Consumer<Response> matcher) {
Request request=builder.get(); // 构建测试输入。
Response response=handler.apply(request); // 执行被选择的处理边界。
matcher.accept(response); // 断言只证明实际处理后的观察结果。
return response;
}
}真实perform通过RequestBuilder创建MockHttpServletRequest,并准备MockHttpServletResponse,而非向操作系统网络端口发请求。
它建立MvcResult并执行MockFilterChain,连接测试Servlet和配置的过滤器,因此MockMvc仍能验证MVC处理链的重要行为。
andExpect调用ResultMatcher检查同一个MvcResult;没有写出的字段或状态不应被默认为已验证。
源码还有异步上下文处理分支,普通同步perform不等同于自动完成所有异步业务;异步测试需要按框架方式进行后续分派与断言。
代码先调用谁,再调用谁
RequestBuilder.buildRequest- 准备模拟请求与响应
MockFilterChain.doFilter- 生成
MvcResult ResultMatcher.match
04动手写一个例子
小规模精确选择测试集合覆盖必需行为
将最多20项必需行为编码为位,每个候选测试有真实覆盖位集合和非负成本,求覆盖全部行为的最小总成本。不可覆盖返回-1。覆盖表由人工和执行证据确认,算法不能给替身测试虚构数据库能力。
- 输入 1
需3项;测试A覆盖001成本2,B覆盖110成本3,C覆盖111成本9- 输出 1
最小成本5
- 输入 2
需2项,只有覆盖01的测试- 输出 2
-1
状态mask表示已得到证据的行为集合。加入一个测试后,新状态是mask与该测试coverage的按位或;成本相加。对每个测试从旧表生成新表,明确每个候选至多选一次。
同一覆盖状态只需保留最小成本,因为后续候选可用性相同,成本更高的方案不会优于较低者。初始只有空集合成本零,其他状态不可达。
集合覆盖一般很难,本题用必需行为数量小的条件进行指数状态动态规划。真实测试选择还要保留冗余故障探测与不稳定性因素,最小成本只能辅助规划,不应机械删除所有重复覆盖测试。
为什么这样写是对的处理前i个测试后,dp[mask]是仅使用这i个候选且恰好形成mask覆盖的最小成本,不可达记为-1。
B为必需行为数,T为候选测试数;每个测试扫描全部位集合状态,保留新旧两张表。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
record TestCase(int coverage,long cost) {}
static long minimum(int bits,List<TestCase> tests) {
if (bits<0||bits>20) throw new IllegalArgumentException("bits");
int states=1<<bits, full=states-1;
long[] dp=new long[states];
Arrays.fill(dp,-1); dp[0]=0;
for (TestCase test:tests) {
if (test.cost()<0||(test.coverage()&~full)!=0)
throw new IllegalArgumentException("test");
// 新旧表分离,保证当前测试最多被选择一次。
long[] next=dp.clone();
for (int mask=0;mask<states;mask++) {
if (dp[mask]<0) continue;
int combined=mask|test.coverage();
long value=Math.addExact(dp[mask],test.cost());
// 相同覆盖仅保留成本较低的方案。
if (next[combined]<0||value<next[combined]) next[combined]=value;
}
dp=next;
}
// 不可覆盖与空要求成本为零有不同返回值。
return dp[full];
}
static void eq(long expected,long actual) {
if (expected!=actual) throw new AssertionError(actual);
}
public static void main(String[] args) {
eq(5,minimum(3,List.of(new TestCase(1,2),new TestCase(6,3),new TestCase(7,9))));
eq(-1,minimum(2,List.of(new TestCase(1,1))));
eq(0,minimum(0,List.of()));
eq(2,minimum(2,List.of(new TestCase(1,1),new TestCase(2,1),new TestCase(3,5))));
eq(0,minimum(1,List.of(new TestCase(1,0))));
try {
minimum(1,List.of(new TestCase(2,1)));
throw new AssertionError("outside coverage");
} catch (IllegalArgumentException expected) {}
System.out.println("PASS 6 test coverage planning tests");
}
}这些特殊情况也要试一试
- 没有必需行为时返回零;有必需行为却无覆盖返回-1。
- 零成本允许,负成本或超出位范围的覆盖拒绝。
- 超过20项拒绝,防止状态空间失控;成本相加使用溢出检测。
进阶选读 · 带风险权重和变更影响的测试选择
场景CI预算有限,但认证与资金相关行为需要更高置信度和多种独立证据。
做法从覆盖图加入风险权重、执行稳定性和独立边界约束;优先选真实触达变更路径的测试,并为关键规则保留至少两种互补验证,而非单纯最小成本。
复杂度增加约束后常需整数规划或启发式;必须报告最优性和覆盖缺口。
好处与代价省时策略可能漏掉未知关联,应定期完整执行并用历史漏检反馈修正覆盖图。
05项目里怎么用
单元测试围绕业务结果写,别把每个内部方法调用都固定成断言。只有业务明确要求不能重复收费、必须避免重复处理或限制资源时,调用次数才是重点。MVC 切片检查参数、校验和错误响应;真实持久化测试再检查 SQL、约束与事务。
集成测试使用可重建的隔离环境与固定迁移脚本,避免依赖开发者手工数据。每个测试创建自己的必要记录并清理,异步任务需要等到确定终态后再结束。事务回滚清理只适用于确实参与该测试事务的操作,不能覆盖任意线程或远程服务。
发布前还要拿真实构建产物检查启动、端口和代理路径,并保存失败请求。普通功能测试通过,不能替代性能、安全或故障恢复验证。报告写明版本、数据规模、实际执行了哪些部分以及没测哪些,让后来的人知道结论适用于什么范围。
容易踩的坑
- Service全部模拟后声称SQL与数据库事务已验证。
MockMvc通过后声称真实端口和代理路径正常。- 真实服务器或后台线程写入依靠测试线程回滚清理。
- 只看构建成功而忽略零测试、跳过或集成测试阶段未运行。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 纯单元测试 | 速度快,边界输入容易构造。 | 不验证容器装配和真实依赖。 | 业务规则与算法。 |
| MVC切片 | 集中验证HTTP契约与框架处理。 | 不覆盖完整持久化和网络。 | 路由、绑定、校验和异常映射。 |
| 真实集成测试 | 验证数据库与组件交互。 | 环境、数据和执行时间较复杂。 | 事务、约束、方言及真实依赖行为。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01SpringBootTest默认一定启动真实网络吗?
展开参考答案与推导
不一定,webEnvironment决定服务器方式,默认模拟环境与RANDOM_PORT不同。
02MockMvc是只调用Controller方法吗?
展开参考答案与推导
不是,它通过模拟Servlet请求和配置的处理链验证MVC行为,但不走真实网络。
03覆盖表能从测试名称直接推断吗?
展开参考答案与推导
不能,应依据实际代码路径、配置和依赖执行证据确认。
换个情况,还会处理吗
01MockMvc成功而网关404怎么办?
展开参考答案与推导
切片未覆盖代理,检查真实路径转发和上游地址并补网络请求。
02测试回滚后后台任务写入仍保留怎么办?
展开参考答案与推导
后台线程通常不加入测试线程事务,需要隔离数据和明确清理。
03CI显示成功但没有测试报告怎么办?
展开参考答案与推导
检查插件绑定、命名发现规则、执行数量与跳过参数,不能判定测试已通过。
写代码试一试
01怎样区分空要求与覆盖不足?
- 输入 / 条件
bits=0以及bits=2仅覆盖01- 输出 / 预期
分别0和-1
提示 定义不可达状态。
展开参考答案与推导
仅dp[0]初始化为0,其余为-1,最后读取full。
02为何每轮要复制dp?
- 输入 / 条件
当前候选测试- 输出 / 预期
每个候选至多使用一次
提示 保持旧状态可用性一致。
展开参考答案与推导
由旧dp产生next,避免当前轮新增状态再次作为选择当前测试的来源。
03如何输出选中了哪些测试?
- 输入 / 条件
A+B比C便宜- 输出 / 预期
A、B及总成本
提示 记录改善状态的前驱。
展开参考答案与推导
为每轮状态记录来自跳过还是选择当前测试,再从最终状态按轮次回溯。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能为同一功能选择单元、切片与集成测试,并准确报告各自证明范围。
本篇重点加练
重点加强行为断言、真实数据库差异与执行证据,检查绿色结果是否验证了目标风险。
- 行为与边界断言
- 真实数据库与方言
- 执行数量与报告证据
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01设计评审
为纯函数“按数量计算折扣”写行为合同,输入含 0、1、门槛前后及非法数量;设计单元测试名称、断言和测试数据,禁止只断言“不抛异常”。
- 提交内容
- 测试清单与可运行单元测试。
- 怎样算完成
- 断言对应可观察行为
- 每个重要边界都有样本
练到的知识点行为与边界断言 · 普通单元测试
- 作业 02编程实践
为一个接收 JSON 的课程 Controller 写 MVC 切片测试,覆盖成功、消息无法解析、校验失败和依赖异常;业务依赖使用可控替身。
- 提交内容
- 测试代码、自写请求样本和实际测试报告。
- 怎样算完成
- 测试经过消息转换和错误映射
- 替身范围与真实被测行为明确
练到的知识点WebMvcTest 切片 · MockMvc 模拟请求
综合应用
- 作业 03原理问答
同一功能分别通过单元测试、
MockMvc、随机端口 Boot 测试和真实数据库测试验证。列出每层能证明什么,以及 SQL 方言、连接配置和网络路径还需哪层覆盖。- 提交内容
- 分层证据矩阵。
- 怎样算完成
- 测试名称不替代实际范围
- 数据库和网络前提明确
练到的知识点SpringBootTest 范围 · 真实数据库与方言
- 作业 04面试场景
测试显示绿色但报告为零条,另一个异步写库测试结束后留下数据。为两种问题分别建立发现、执行、隔离和清理证据链。
- 提交内容
- 排查清单、时间线与修复验收条件。
- 怎样算完成
- 执行数量实际可查
- 异步任务完成与清理次序明确
练到的知识点测试数据和异步事务 · 执行数量与报告证据
- 作业 05排错实验
在独立测试库运行创建课程的集成测试,主动制造一次断言失败和一次数据库约束失败;确保每次结束后只清理本测试拥有的数据并可连续重复运行。
- 提交内容
- 集成测试、自写清理/重复运行检查及测试报告。
- 怎样算完成
- 失败也保持环境可复现
- 不误删其他数据
- 真实数据库行为可观察
练到的知识点行为与边界断言 · SpringBootTest 范围 · 真实数据库与方言 · 测试数据和异步事务
- 作业 06编程实践
为课程容量校验写测试,再在练习副本中故意把一个边界比较改错;确认原测试是否能发现,必要时补充能说明需求的断言。
- 提交内容
- 测试、故障副本、自写边界样本和前后报告。
- 怎样算完成
- 测试失败关联具体需求
- 实际执行数量有记录
练到的知识点行为与边界断言 · 执行数量与报告证据
- 作业 07排错实验
选一个依赖目标数据库行为的查询或约束,用隔离数据库检验;若另有内存替代库,对同一输入记录差异,不要求结果必须不同。
- 提交内容
- SQL、测试数据、自写结果断言与数据库版本证据。
- 怎样算完成
- 测试目标基于真实方言合同
- 观察没有被过度推广
练到的知识点真实数据库与方言
- 作业 08设计评审
报名接口依赖事务与唯一约束,单元测试用内存
Map替身。列出替身可证明与需真实资源证明的行为,提出最小补充测试。- 提交内容
- 行为证据矩阵与补充用例。
- 怎样算完成
- 替身边界可解释
- 数据库风险有对应测试入口
练到的知识点行为与边界断言 · 真实数据库与方言
- 作业 09面试场景
同名测试在 IDE 执行三条、命令行只执行两条。追踪测试发现条件、过滤参数与报告路径,建立提交前必须核验的证据。
- 提交内容
- 发现链与执行差异排查表。
- 怎样算完成
- 测试名称与运行标识可关联
- 成功码不替代执行集合确认
练到的知识点执行数量与报告证据
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
完成覆盖分析后:CI 预算只有 3 分钟,需要快速阻止高风险回归。按行为与故障影响选择测试集合,注明延期运行的测试以及缺口风险。
- 提交内容
- 行为覆盖表、耗时记录与分层运行方案。
- 怎样算完成
- 不把代码覆盖率等同需求覆盖
- 选择依据包含风险与证据成本
练到的知识点行为与边界断言 · 普通单元测试 · SpringBootTest 范围 · 执行数量与报告证据
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
检验“只要断言数量很多,测试就能发现关键错误”。构造一个具体需求与一组看似丰富的断言,说明怎样用受控故障检验其有效性。
- 提交内容
- 需求、断言清单和故障验证记录。
- 怎样算完成
- 评价依据是行为缺陷
- 不以断言数量作为唯一指标
练到的知识点行为与边界断言 · 执行数量与报告证据
- 作业 12思维拓展 · 条件变化
目标数据库升级后 SQL 返回顺序变化,但接口未声明排序。分析测试应锁定哪些行为、哪些属于偶然实现,提出合同与测试的共同修订方案。
- 提交内容
- 升级样本、合同决定和测试变更理由。
- 怎样算完成
- 业务需要与观察到的顺序分开
- 真实数据库前提保留
练到的知识点行为与边界断言 · 真实数据库与方言
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Spring Boot 3.4:测试应用 (新标签页)↗
比较普通测试、
WebMvcTest切片和SpringBootTest,明确每类测试的证明范围。 - 实践实践指南英文
JUnit 5.11:用户指南 (新标签页)↗
阅读断言、异常断言、参数化测试和生命周期;按 Spring Boot BOM 锁定兼容版本。
- 深入参考文档英文
Spring MVC:RequestBody (新标签页)↗
阅读消息转换器、请求体绑定和校验失败行为,区分 JSON 与表单参数。
- 深入参考文档英文
Spring:事务回滚规则 (新标签页)↗
核对默认异常规则和自定义配置,理解捕获异常与 rollback-only 的影响。
- 实践实践指南中文
第一个 JUnit 测试:把正确结果写成自动检查 (新标签页)↗
先学创建测试类和运行测试。小练习:给求和方法写普通输入、空输入、负数三个测试,故意写错一个期望值,读懂失败提示后再修正。 文档公开;示例含较旧的 JUnit 版本和 Java 21 设置。可学习操作步骤,依赖版本应与自己的项目匹配。
- 实践实践指南英文
Spring:给控制器写第一组测试 (新标签页)↗
先读
contextLoads,再读 @WebMvcTest与服务替身示例;暂跳 Kotlin 和真实端口测试。给同一控制器写正常返回、服务返回异常两组检查,交出测试结果和一段说明:本次到底加载了哪些组件。 网页公开可读,无需登录。运行需要匹配项目版本的 JDK 和构建工具;当前示例的测试包名与旧版 Spring Boot 不完全相同,先核对自己的版本。 - 实践实践指南英文
Testcontainers:让测试自己准备 Redis (新标签页)↗
顺着第 1–3 步和完整测试类阅读,先跳并行启动与复用容器。练习写入一个键再读回;只用
getHost与实际映射端口连接。交出测试代码,并解释为什么不能把宿主机端口写死。 文档公开可读,无需登录。运行需要 Java 构建环境、兼容 Docker API 的容器运行时,以及拉取镜像的网络与磁盘空间;云服务不是必选项。示例中的依赖与镜像版本需结合自己的项目核对。 - 深入参考文档英文
Spring6.2测试事务 (新标签页)↗
核对线程绑定事务与预抢占超时等限制。
- 深入参考文档英文
MockMvc源码 (新标签页)↗
查看模拟请求构造、过滤链和结果断言路径。