qinghuan
← Java Web / 全部笔记

单元 67 / Java Web

P0 · 必会主线L2 · 进阶

Boot 测试分层:单元、MVC 切片与集成验证

按要验证的行为选择单元、MVC 切片或集成测试,并说明哪些依赖真实运行、哪些只是模拟。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能为同一功能选择单元、切片与集成测试,并准确报告各自证明范围。

学习理由、准备要求与知识点

为什么学它

测试范围必须与要证明的行为相符,才能避免用模拟通过冒充真实链路验证。

先读这些内容

知识点清单

  • 行为与边界断言
  • 普通单元测试
  • 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["报告实际覆盖与未覆盖"]

按顺序看,每一步做了什么

  1. 写出成功、非法输入、未授权和异常路径的期望。
  2. 为每条期望选最小但足够的执行范围。
  3. 明确哪些协作者真实执行,哪些使用替身。
  4. 安排数据隔离、异步等待与清理,不依赖测试顺序。
  5. 检查执行数量、报告与目标环境,限定测试结论。

03对照实现代码

MockMvc.perform的模拟请求链 (新标签页)↗

Spring Framework v6.2.11 · MockMvc#perform(RequestBuilder)

原创教学实现,非源码节选。不实现Servlet或Spring,只保留请求构造、处理与结果断言的分离;真实MockMvc还处理过滤器、请求上下文与异步分派。

核心机制代码 · java
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不等同于自动完成所有异步业务;异步测试需要按框架方式进行后续分派与断言。

代码先调用谁,再调用谁

  1. RequestBuilder.buildRequest
  2. 准备模拟请求与响应
  3. MockFilterChain.doFilter
  4. 生成MvcResult
  5. 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。

时间 O(T×2^B)额外空间 O(2^B)

B为必需行为数,T为候选测试数;每个测试扫描全部位集合状态,保留新旧两张表。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 41 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能为同一功能选择单元、切片与集成测试,并准确报告各自证明范围。

本篇重点加练

重点加强行为断言、真实数据库差异与执行证据,检查绿色结果是否验证了目标风险。

  • 行为与边界断言
  • 真实数据库与方言
  • 执行数量与报告证据

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01设计评审

    为纯函数“按数量计算折扣”写行为合同,输入含 0、1、门槛前后及非法数量;设计单元测试名称、断言和测试数据,禁止只断言“不抛异常”。

    提交内容
    测试清单与可运行单元测试。
    怎样算完成
    • 断言对应可观察行为
    • 每个重要边界都有样本

    练到的知识点行为与边界断言 · 普通单元测试

  2. 作业 02编程实践

    为一个接收 JSON 的课程 Controller 写 MVC 切片测试,覆盖成功、消息无法解析、校验失败和依赖异常;业务依赖使用可控替身。

    提交内容
    测试代码、自写请求样本和实际测试报告。
    怎样算完成
    • 测试经过消息转换和错误映射
    • 替身范围与真实被测行为明确

    练到的知识点WebMvcTest 切片 · MockMvc 模拟请求

综合应用

  1. 作业 03原理问答

    同一功能分别通过单元测试、MockMvc、随机端口 Boot 测试和真实数据库测试验证。列出每层能证明什么,以及 SQL 方言、连接配置和网络路径还需哪层覆盖。

    提交内容
    分层证据矩阵。
    怎样算完成
    • 测试名称不替代实际范围
    • 数据库和网络前提明确

    练到的知识点SpringBootTest 范围 · 真实数据库与方言

  2. 作业 04面试场景

    测试显示绿色但报告为零条,另一个异步写库测试结束后留下数据。为两种问题分别建立发现、执行、隔离和清理证据链。

    提交内容
    排查清单、时间线与修复验收条件。
    怎样算完成
    • 执行数量实际可查
    • 异步任务完成与清理次序明确

    练到的知识点测试数据和异步事务 · 执行数量与报告证据

  3. 作业 05排错实验

    在独立测试库运行创建课程的集成测试,主动制造一次断言失败和一次数据库约束失败;确保每次结束后只清理本测试拥有的数据并可连续重复运行。

    提交内容
    集成测试、自写清理/重复运行检查及测试报告。
    怎样算完成
    • 失败也保持环境可复现
    • 不误删其他数据
    • 真实数据库行为可观察

    练到的知识点行为与边界断言 · SpringBootTest 范围 · 真实数据库与方言 · 测试数据和异步事务

  4. 作业 06编程实践

    为课程容量校验写测试,再在练习副本中故意把一个边界比较改错;确认原测试是否能发现,必要时补充能说明需求的断言。

    提交内容
    测试、故障副本、自写边界样本和前后报告。
    怎样算完成
    • 测试失败关联具体需求
    • 实际执行数量有记录

    练到的知识点行为与边界断言 · 执行数量与报告证据

  5. 作业 07排错实验

    选一个依赖目标数据库行为的查询或约束,用隔离数据库检验;若另有内存替代库,对同一输入记录差异,不要求结果必须不同。

    提交内容
    SQL、测试数据、自写结果断言与数据库版本证据。
    怎样算完成
    • 测试目标基于真实方言合同
    • 观察没有被过度推广

    练到的知识点真实数据库与方言

  6. 作业 08设计评审

    报名接口依赖事务与唯一约束,单元测试用内存 Map 替身。列出替身可证明与需真实资源证明的行为,提出最小补充测试。

    提交内容
    行为证据矩阵与补充用例。
    怎样算完成
    • 替身边界可解释
    • 数据库风险有对应测试入口

    练到的知识点行为与边界断言 · 真实数据库与方言

  7. 作业 09面试场景

    同名测试在 IDE 执行三条、命令行只执行两条。追踪测试发现条件、过滤参数与报告路径,建立提交前必须核验的证据。

    提交内容
    发现链与执行差异排查表。
    怎样算完成
    • 测试名称与运行标识可关联
    • 成功码不替代执行集合确认

    练到的知识点执行数量与报告证据

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    完成覆盖分析后:CI 预算只有 3 分钟,需要快速阻止高风险回归。按行为与故障影响选择测试集合,注明延期运行的测试以及缺口风险。

    提交内容
    行为覆盖表、耗时记录与分层运行方案。
    怎样算完成
    • 不把代码覆盖率等同需求覆盖
    • 选择依据包含风险与证据成本

    练到的知识点行为与边界断言 · 普通单元测试 · SpringBootTest 范围 · 执行数量与报告证据

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    检验“只要断言数量很多,测试就能发现关键错误”。构造一个具体需求与一组看似丰富的断言,说明怎样用受控故障检验其有效性。

    提交内容
    需求、断言清单和故障验证记录。
    怎样算完成
    • 评价依据是行为缺陷
    • 不以断言数量作为唯一指标

    练到的知识点行为与边界断言 · 执行数量与报告证据

  2. 作业 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源码 (新标签页)↗

    查看模拟请求构造、过滤链和结果断言路径。

打开资料库,找下一份学习材料 ↗