单元测试、JUnit 与 Mockito:验证行为而不绑定实现
“订单金额满 100 元免运费,否则收取 6 元;金额不能为负数”包含三条可以直接执行的规则。金额以分存储后,最有区分度的输入是 -1、0、9999、10000 和 10001。其中 10000 能区分“大于”和“大于等于”,普通的 20000 成功样例做不到。
单元测试把这类规则固定在小范围代码中。被调用的是实际定价函数,数据库、支付网络等协作者可以由测试替身替换;每次失败都应指向一个具体输入、结果或必要交互。
把业务规则变成第一组 JUnit 测试
测试代码、引擎与构建工具
Java 测试工程的常用结构如下:
testing-unit/
├── pom.xml
├── src/main/java/example/testing/
│ ├── ShippingFee.java 运费规则
│ ├── Checkout.java 调用支付端口的结算服务
│ └── OrderIds.java 静态方法作用域示例
└── src/test/java/example/testing/
├── ShippingFeeTest.java 参数化规则表
├── CheckoutTest.java 协作者输入与调用次数
├── MockitoMechanicsTest.java
├── LifecycleTest.java
├── BrokenBoundaryCase.java 单独运行的阈值反例
└── UnusedStubCase.java 单独运行的未用桩反例JUnit 的职责也分层。Platform 提供测试发现、启动和 TestEngine 接口;Jupiter 提供 @Test、断言、参数化和扩展机制,以及执行这些测试的引擎;Vintage 用于过渡执行旧 JUnit 3/4 测试。JUnit 6 要求测试运行 JVM 为 Java 17 或更高,测试对象本身可以是更低版本编译的字节码。Vintage 已弃用,适合作为旧测试迁移通道。JUnit 6.0.3 概览
Maven Surefire 在 test 阶段选择测试类,委托 Platform 发现并执行。安装了注解 API 但没有可用引擎、类名不符合发现规则、tag 被排除,都会影响实际执行集合。IDE 的单次绿色与 Maven 整套执行需要分别检查。
在 Linux 中运行示例
下载 完整单元测试工程,解压后进入包内的 testing-unit 目录。工程使用 JUnit 6.0.3、Mockito 5.23.0、Maven 3.9.12,编译目标为 Java 17,可在 Java 17 和 25 上运行;不需要启动 Spring 或数据库。
POM 用 JUnit BOM 统一 Platform 和 Jupiter 的版本,测试依赖声明为 test 作用域。junit-jupiter 聚合 API、参数化支持和引擎;mockito-junit-jupiter 提供 Mockito 的 Jupiter Extension,并传递引入 Mockito Core:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
</dependencies>Linux 宿主需要 unzip、Docker CLI,以及访问开发用 Docker daemon 的权限。用当前普通用户执行:
unzip testing-unit-lab.zip
cd testing-unit
mkdir -p .m2
LAB_DIR="$(pwd -P)"
MAVEN_IMAGE='maven:3.9.12-eclipse-temurin-25'
docker version
docker pull "$MAVEN_IMAGE"docker version 应同时显示 Client 和 Server。只有 Client 或出现 permission denied 时,先修复开发环境的 daemon/socket 访问,不能继续把后续失败当测试错误。Docker 安装入口见 Docker Engine 官方安装文档;用户组拥有很高的 daemon 操作权限,权限含义见 Linux 安装后配置。
先只运行运费测试,避免首次输出同时包含多个机制:
docker run --rm --user "$(id -u):$(id -g)" \
-e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
--mount "type=bind,src=$LAB_DIR,dst=/work" \
--mount "type=bind,src=$LAB_DIR/.m2,dst=/m2" \
--workdir /work "$MAVEN_IMAGE" \
mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/m2 \
-Dtest=ShippingFeeTest test这里有三个不同身份:宿主用户发起 Docker 请求;Maven 进程按宿主 UID/GID 运行并写入当前用户创建的目录;daemon 负责创建容器。没有把 Docker socket 挂进此容器,测试代码本身不需要创建其他容器。HOME、MAVEN_CONFIG 与 Maven 的 user.home 分别覆盖 Shell 环境、镜像入口配置和 JVM 用户目录,避免普通 UID 去写 /root。
正常结果包含 Tests run: 6, Failures: 0, Errors: 0, Skipped: 0。五个参数化输入各计一次执行,负数异常另计一次;target/surefire-reports 中的 XML 和文本报告保存方法名、参数和失败栈。
如果宿主已装对应 JDK 与 Maven,在同一目录直接执行 mvn -B -ntp -Dtest=ShippingFeeTest test。两条路径使用相同 POM。企业内网通过组织维护的 Maven mirror 和认证配置下载依赖,镜像来自获准仓库;将脱敏的 settings.xml 只读挂载后使用 mvn -s。离线运行须先在可联网环境完成相同目标,准备依赖和构建插件缓存,再用 -o 检查是否齐全。配置方式见 Maven Settings Reference,不要把个人代理口令写入示例 POM。
规格表与断言应当独立于实现
生产规则只有一个入口:
public final class ShippingFee {
public long cents(long subtotalCents) {
if (subtotalCents < 0) {
throw new IllegalArgumentException("NEGATIVE_SUBTOTAL");
}
return subtotalCents >= 10_000 ? 0 : 600;
}
}测试输入和 expected 来自业务表,不再调用一遍运费算法计算 expected:
@ParameterizedTest(name = "subtotal={0}, shipping={1}")
@CsvSource({"0,600", "1,600", "9999,600", "10000,0", "10001,0"})
void shippingAtBoundaries(long subtotal, long expected) {
assertEquals(expected, new ShippingFee().cents(subtotal));
}
@Test
void negativeSubtotalHasStableError() {
var error = assertThrows(IllegalArgumentException.class,
() -> new ShippingFee().cents(-1));
assertEquals("NEGATIVE_SUBTOTAL", error.getMessage());
}测试的准备、调用、观察分别对应 Arrange、Act、Assert。短函数可以把调用写进断言;涉及状态准备和外部协作时分开写更容易定位失败。assertEquals 比较值,assertSame 比较对象身份;异常断言返回捕获的异常,便于继续检查稳定错误码。assertThrows 接受指定类型的子类,要求类型精确时用 assertThrowsExactly。JUnit 断言
金额测试还要根据真正的金额模型检查币种、精度、舍入和溢出。例如 BigDecimal.equals 会比较 scale;若业务只比较数值,可显式比较 compareTo,若 scale 本身属于协议则应单独断言。不要为了规避测试失败,随意把金额统一转换为 double。
输入表只是规则的一个维度。集合常见输入有空、单项、重复与上限;状态对象要区分合法迁移、重复动作和终态拒绝;字符串关注空值、空串、空白、编码与长度。这些选择由函数接受的数据决定。
用 Mockito 替换外部协作
单元可以由几个真实对象组成
结算服务使用真实 ShippingFee,只替换支付端口。对象关系是:
Checkout
├── ShippingFee 实际规则对象
├── Clock 固定时间输入
└── PaymentPort 外部协作接口
└── Mockito mock 回应 charge,并记录调用数据库、远程调用、当前时间和随机 ID 会增加外部状态。把它们通过构造参数传入,可以独立准备输入,也让生产代码对协作者的依赖可见。值对象、集合和纯计算通常直接使用真实对象,不需要给每个类创建 mock。
测试替身有不同用途:Dummy 只填充当前用不到的参数;Stub 提供预设输入;Fake 用较轻实现替代完整服务;记录型 Spy 保存调用供检查;Mock 用于配置响应和验证交互。Mockito 的 spy 则特指通常调用真实方法的部分替身,不能把两种术语简单对应成同一个实现。一个 Mockito mock 可以同时承担 Stub 和交互验证角色。
内存 Repository Fake 适合运行服务分支,但它自己的事务、唯一约束和排序未必与目标数据库相同。需要检查 SQL 和提交结果时,改用 Spring 集成测试,不要继续扩充 Fake 来模拟整套数据库。
准备返回值,再检查发送出去的命令
工程中的 Checkout 定义了以下协作接口:
public record Charge(String operationId, long totalCents, Instant requestedAt) { }
public record Receipt(String paymentId) { }
public interface PaymentPort {
Receipt charge(Charge request);
}pay 先检查 operationId、计算包含运费的总额,再调用一次 payment.charge。Mockito Extension 创建 @Mock 字段,测试中显式构造服务,便于看清哪些对象被替换:
@ExtendWith(MockitoExtension.class)
class CheckoutTest {
@Mock PaymentPort payment;
private final Clock clock = Clock.fixed(Instant.EPOCH, ZoneOffset.UTC);
private Checkout checkout;
@BeforeEach
void prepare() {
checkout = new Checkout(new ShippingFee(), payment, clock);
}
@Test
void submitsOneChargeWithCallerOperationId() {
var receipt = new Receipt("PAY-1");
when(payment.charge(any(Charge.class))).thenReturn(receipt);
assertSame(receipt, checkout.pay("OP-1", 9_999));
var request = ArgumentCaptor.forClass(Charge.class);
verify(payment).charge(request.capture());
assertAll(
() -> assertEquals("OP-1", request.getValue().operationId()),
() -> assertEquals(10_599, request.getValue().totalCents()),
() -> assertEquals(Instant.EPOCH, request.getValue().requestedAt()));
verifyNoMoreInteractions(payment);
}
}when(...).thenReturn(...) 决定依赖怎样回应,verify 检查服务实际发出了什么。verify(payment) 默认要求一次匹配调用;Captor 在验证时捕获参数。这里检查 operationId、金额和请求时间,是因为它们会进入支付协议。若只验证“返回的是刚配置的 receipt”,漏算运费仍可能通过。
拒绝路径不需要配置成功桩:
assertThrows(IllegalArgumentException.class,
() -> checkout.pay("OP-2", -1));
verifyNoInteractions(payment);异常路径则让支付端口抛出错误,再检查错误传播与调用次数。服务没有约定重试,就不应悄悄发出第二次扣款。使用 verifyNoMoreInteractions 的地方应当存在这种“额外调用会产生副作用”的理由;对内部 getter 和无关查询全部验证次数,会把重构变成修改测试的工作。
调用为什么会落到错误的桩
Mockito 保存 mock 的调用记录与已配置的 stubbing。调用到达时,参数匹配器参与选择 Answer;匹配成功后执行配置的返回或异常,未匹配时使用默认 Answer。普通对象返回值经常是 null,基本类型有零值,部分集合和 Optional 有空值行为,具体由默认 Answer 定义。
service 调用 mock.method(arguments)
│
├── 保存 invocation,供 verify/Captor 使用
│
└── 查找匹配的 stubbing
├── 匹配:执行 Answer,标记该 stubbing 被使用
└── 未匹配:使用默认 Answer,或被 strictness 检测为参数问题
测试结束 → strictness 检查未使用 stubbing参数 matcher 只描述选择条件,不是可供业务代码使用的普通返回值。一个方法只要使用了 matcher,其他参数也应使用 matcher,例如 eq("OP-1") 配合 any(Charge.class)。any(SomeType.class) 通常不会匹配 null,nullable 输入使用明确的 null matcher。相关方法、默认 Answer 和 spy 语义在 Mockito 5.23.0 官方 API 源文档中有完整说明。
生产代码走了另一分支、参数单位从元变成分、或测试配置了实际上不使用的协作者,都可能造成桩未命中。此时先比较实际 invocation 的参数与 stubbing,不要马上把匹配放宽为全部 any()。
严格桩检查发生在调用时和收尾时
Mockito 的严格桩模式会检查参数不匹配和未使用配置,但两者触发时间不同。PotentialStubbingProblem 属于调用过程中的参数差异诊断;UnnecessaryStubbingException 常在测试方法已经返回后,由 Extension 收尾发现。后者会让一个“方法体没有抛错”的测试仍然失败。Mockito strictness 定义
可以只运行工程中的未使用桩反例。沿用前面的 Linux 目录与变量:
set +e
docker run --rm --user "$(id -u):$(id -g)" \
-e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
--mount "type=bind,src=$LAB_DIR,dst=/work" \
--mount "type=bind,src=$LAB_DIR/.m2,dst=/m2" \
--workdir /work "$MAVEN_IMAGE" \
mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/m2 \
-Dtest=UnusedStubCase test > unused-stub.log 2>&1
status=$?
set -e
test "$status" -ne 0
grep -F 'UnnecessaryStubbingException' unused-stub.log预期是 Maven 非零退出,日志指出未使用的 payment.charge 配置行。set +e 只让 Shell 有机会保存这个预期错误的退出码;后面的检查仍会在没有失败或异常类型错误时返回非零。修复是删除不属于该场景的桩,或者纠正测试的输入和被调用路径。lenient() 可以局部放过特殊配置,但不适合作为整个项目的默认降噪开关。
MockitoMechanicsTest 还直接启动一个严格 MockitoSession,用 assertThrows 检查 finishMocking 抛出同一异常。这是对框架收尾行为的正常测试;UnusedStubCase 则让 Extension 自己报告失败,两者用途不同。
Spy 和静态 Mock 有真实副作用与作用域
给空 ArrayList 的第零项配置返回时,下面两种写法行为不同:
var values = spy(new ArrayList<String>());
assertThrows(IndexOutOfBoundsException.class,
() -> when(values.get(0)).thenReturn("x"));
doReturn("x").when(values).get(0);
assertEquals("x", values.get(0));
assertTrue(values.isEmpty());Java 必须先求值 values.get(0),才能进入 when;spy 默认调用真实方法,因此第一种写法在准备阶段就抛异常。doReturn 先建立配置,不执行那次真实读取。对文件、网络或数据库对象使用 spy 时,同样可能在准备阶段触发副作用。
静态 mock 通过 try-with-resources 限定生命周期:
try (var ids = mockStatic(OrderIds.class)) {
ids.when(OrderIds::next).thenReturn("FIXED");
assertEquals("FIXED", OrderIds.next());
}
assertTrue(OrderIds.next().startsWith("ORD-"));MockedStatic 作用于创建它的线程,关闭后解除当前作用域。把它留在静态字段里,或希望它自动影响线程池中的调用,都容易产生错误。可修改的业务设计更适合注入 ID 生成器;静态 mock 主要用于旧接口过渡和局部兼容测试。MockedStatic 契约
让场景表、生命周期和时间保持可理解
根据输入形态选择测试组织方式
简单值使用 @ValueSource,二维规则表使用 @CsvSource,复杂对象或多参数组合使用 @MethodSource。参数名称应反映规格,例如金额、身份和预期状态;失败报告只显示一串对象地址时,应为对象补稳定的描述或显式 display name。参数化测试
@Nested 适合表达同一个初始状态下的多种行为,例如“已支付订单”下取消、确认和重复确认。嵌套层次太深会让 Fixture 来源难找,状态较多时直接使用工厂函数准备目标状态更清楚。
@TestFactory 可以从动态数据生成测试,但动态子测试不会各自获得一次 @BeforeEach/@AfterEach;生命周期回调围绕工厂方法执行。动态节点共享工厂准备的可变状态时,要自行隔离。普通规格表优先采用参数化测试。动态测试生命周期
每方法实例与每类实例
Jupiter 默认 PER_METHOD:为各测试创建新实例,实例字段不会天然跨测试共享;PER_CLASS 复用一个实例,可以使用非静态 @BeforeAll,也要求更谨慎地处理可变字段。静态变量、外部文件和数据库不因新建测试实例而自动清理。测试实例生命周期
典型执行顺序可按作用域拆开:
测试类:BeforeAll
测试 A:新实例 → 扩展/BeforeEach → Test → AfterEach/扩展收尾
测试 B:新实例 → 扩展/BeforeEach → Test → AfterEach/扩展收尾
测试类:AfterAllExtension 的前后回调会包围用户方法,多个扩展的注册与回调顺序由各接口约定决定,不能从这个简图推导任意扩展都先于或晚于其他扩展。MockitoExtension 管理 mock 会话,JUnit 内建 @TempDir 管理临时目录;自己创建的连接、Executor 或文件流仍要在确定的 owner 中关闭。JUnit 扩展模型
工程的 LifecycleTest 用两个方法分别断言初始化计数为 1,并在 @TempDir 中写入和读取文件。两个方法各自准备状态,也可以单独选择运行。
固定业务时间,限制真实等待
结算测试注入 Clock.fixed(Instant.EPOCH, ZoneOffset.UTC)。它固定业务输入,不需要把机器时钟改成某个时间。过期边界可以使用同一 Clock 的 offset,分别测试截止前、截止时、截止后。
异步任务需要等待的是完成条件。Future.get(timeout, unit) 既给等待设上限,也把工作线程异常送回测试线程;纯 Thread.sleep 后直接结束测试,后台失败可能根本未被断言看到。任务结束还应关闭 executor 并等待退出,避免下一方法继承迟到任务。
JUnit assertTimeout 在调用线程运行,完成后判断是否超过预算;它不能强行中止一个永不返回的函数。assertTimeoutPreemptively 使用另一线程,超时可以返回失败,但被测任务能否实际停止仍取决于取消与中断响应;ThreadLocal 事务也不会自动跟过去。涉及 Spring 测试事务时尤其要留意,这可能让工作线程真实提交而主测试回滚。JUnit 超时
性质测试还可以检查排序幂等、序列化往返和金额守恒等关系。性质必须来自业务,随机失败应保存 seed 和最小输入;随机样本不能代替运费恰好等于阈值这类指定例子。需要系统性生成与缩减反例时使用相应性质测试框架,其配置独立于普通 Jupiter 参数化。
运行、反例和常见失败
看见失败,再恢复正常套件
运行阈值反例时选择 BrokenBoundaryCase。这个测试包含一个故意使用 > 10000 的错误实现,在金额恰好等于阈值时断言失败:
set +e
docker run --rm --user "$(id -u):$(id -g)" \
-e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
--mount "type=bind,src=$LAB_DIR,dst=/work" \
--mount "type=bind,src=$LAB_DIR/.m2,dst=/m2" \
--workdir /work "$MAVEN_IMAGE" \
mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/m2 \
-Dtest=BrokenBoundaryCase test > boundary.log 2>&1
status=$?
set -e
test "$status" -ne 0
grep -F 'expected: <0> but was: <600>' boundary.log预期是 Maven 非零退出,错误输出包含 expected: <0> but was: <600>。将反例中的比较符恢复为 >= 后,单独执行它应转为成功。如果只测试高于阈值的金额,这个错误会被遗漏。
两个 *Case 通过 -Dtest 单独选择,正常 Surefire 命名模式选择 *Test。运行所有正常测试并重新生成报告:
docker run --rm --user "$(id -u):$(id -g)" \
-e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
--mount "type=bind,src=$LAB_DIR,dst=/work" \
--mount "type=bind,src=$LAB_DIR/.m2,dst=/m2" \
--workdir /work "$MAVEN_IMAGE" \
mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/m2 clean verify正常套件共有 18 次执行,结果为零失败、零错误、零跳过。clean 移除旧的目标产物,避免把上一轮失败报告误看成这轮结果。成功后查看各 TEST-*.xml 的 tests、failures、errors 和 skipped,而非只看最后一行。缓存保留在 .m2 供后续运行,--rm 删除退出的 Maven 容器;日志是否保留由当前排查需要决定。
选择一个方法可用 -Dtest=CheckoutTest#rejectsNegativeSubtotalBeforeCallingPayment,按 tag 可用 -Dgroups=unit。在多模块工程中同时确认被选择模块里确实有用例。Surefire 默认命名、includes/excludes 和选择语法见 Surefire JUnit Platform。
未发现测试与依赖不一致
出现 No tests were executed,按以下顺序处理:
- 确认代码在
src/test/java,导入的是org.junit.jupiter.api.Test,类和方法没有被隐藏条件或过滤器排除。 - 用
-Dtest=ShippingFeeTest明确选择已知类;若能执行,问题在发现规则而非业务方法。 - 运行
mvn -B -ntp dependency:tree '-Dincludes=org.junit.*,org.mockito',检查 API、Engine 和 Platform 是否被不一致的传递依赖覆盖。 - 清理旧目标目录后重跑原选择,再核对报告数量。
JUnit BOM 管理同一发行线的各模块。升级引擎时,除编译和绿色结果外,还应比较原有测试、参数化调用、tag、跳过状态和旧 Vintage 测试是否仍被执行。Maven 与其他构建工具的支持配置见 JUnit Build Support。
JDK 21+ 的 Mockito agent
Mockito 5 默认使用 inline mock maker,涉及 final 或静态方法时依靠字节码 instrumentation。较新 JDK 对动态加载 agent 给出警告并逐步加强限制。测试工程显式在测试 JVM 启动时加载 Mockito agent,避免依赖运行中的自附加行为。JEP 451
POM 在 process-test-classes 把 mockito-core 复制到 target/agents/mockito.jar,Surefire 使用:
<configuration>
<failIfNoTests>true</failIfNoTests>
<argLine>@{argLine} -javaagent:${project.build.directory}/agents/mockito.jar</argLine>
</configuration>@{argLine} 保留其他插件在稍后阶段加入的 JVM 参数;它对应的属性在 POM 中初始化为空。接入 JaCoCo 时直接覆盖 argLine,可能让 Mockito 或覆盖 agent 中的一方消失。报 Could not initialize inline Byte Buddy mock maker 时先看有效测试 JVM 启动参数、agent 文件和 Byte Buddy/JDK 组合,不要通过禁用全部测试绕过。Mockito 的显式 agent 用法与局限见上面的固定版本 API 源文档。
桩错误与业务错误分别修复
| 首个错误 | 先看什么 | 修复后运行什么 |
|---|---|---|
Wanted but not invoked | 实际服务对象是否拿到这个 mock,拒绝条件是否提前返回 | 同一场景,继续检查返回值和副作用 |
UnnecessaryStubbingException | 报告指向的 stubbing 是否属于当前路径 | 删除多余准备后跑单类及相关套件 |
PotentialStubbingProblem | 调用参数与桩参数,特别是单位、空值和 ID | 原始输入加邻近边界输入 |
| spy 准备阶段抛错 | when 中是否已经执行真实方法 | doReturn 或提取明确协作者后复跑 |
| 返回 null 后业务 NPE | 未匹配桩、默认 Answer,还是业务允许空结果 | 分别测试合法结果和缺失结果 |
| 只在整套中失败 | 静态 mock、系统属性、线程和共享 Fixture | 原失败顺序及隔离测试,见 Fixture 与 Flaky Test |
测试允许修改私有方法拆分、局部变量和无关调用顺序。需要修改测试的应当是外部行为变化:运费规则、错误码、支付命令、调用次数或可见状态。这个分界让测试既能抓住缺陷,也能给重构留下空间。
权威资料与规范地址
JUnit 与构建
- JUnit 6.0.3 模块和 Java 要求:https://docs.junit.org/6.0.3/overview.html
- 断言:https://docs.junit.org/6.0.3/writing-tests/assertions.html
- 参数化测试:https://docs.junit.org/6.0.3/writing-tests/parameterized-classes-and-tests.html
- 动态测试:https://docs.junit.org/6.0.3/writing-tests/dynamic-tests.html
- 实例生命周期:https://docs.junit.org/6.0.3/writing-tests/test-instance-lifecycle.html
- 扩展模型:https://docs.junit.org/6.0.3/extensions/overview.html
- 超时:https://docs.junit.org/6.0.3/writing-tests/timeouts.html
- 构建工具支持:https://docs.junit.org/6.0.3/running-tests/build-support.html
- Surefire JUnit Platform:https://maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html
- Maven settings:https://maven.apache.org/settings.html
Mockito 与运行环境
- Mockito 5.23.0 API 源文档:https://github.com/mockito/mockito/blob/v5.23.0/mockito-core/src/main/java/org/mockito/Mockito.java
- Strictness:https://github.com/mockito/mockito/blob/v5.23.0/mockito-core/src/main/java/org/mockito/quality/Strictness.java
- MockedStatic:https://github.com/mockito/mockito/blob/v5.23.0/mockito-core/src/main/java/org/mockito/MockedStatic.java
- JEP 451:https://openjdk.org/jeps/451
- Docker Engine 安装:https://docs.docker.com/engine/install/
- Docker Linux 安装后配置:https://docs.docker.com/engine/install/linux-postinstall/
