资讯动态

JUnit 5+Mockito:让单元测试摆脱外部依赖的实战指南

发布时间:2026/10/20 8:37:29 来源:尧图企业网站定制
1. 先说点实话为什么测试代码比业务代码更让人头疼我以前接过一个项目业务代码写得还算规矩但一到跑测试就头大——测试类启动要十几秒数据库一换环境就全部变红同事为了绕过外部依赖直接用if (testMode)这种开关去改业务代码。后来我把整个测试体系推倒重来核心工具就是 JUnit 5 加 Mockito。这两个东西单独拎出来都不算新但把它们真正集成好能解决一个很实际的问题让单元测试不再依赖数据库、网络、消息队列这些外部因素只测你当前这段代码的逻辑本身。这篇博文适合三类人看刚接触单元测试、想搞清楚 Mock 和 InjectMocks 到底怎么用的新手正在从 JUnit 4 往 JUnit 5 迁移、被版本兼容问题卡住的开发者以及已经写了大量测试但经常被打桩失效静态方法无法 mock之类问题折磨的老手。我不会只列 API我会把我踩过的坑、排查思路、以及最终沉淀下来的写法习惯全部展开你照着做就能少折腾好几个晚上。先说一个最根本的观念单元测试要测的是这个类的行为对不对而不是它和外部系统能不能配合好。你测试一个遥控器不需要真的打开一台电视只需要一台假电视按下按钮后能验证红外信号发出去了就行。Mockito 就是帮你造假电视的那个工具JUnit 5 则是负责把这些假对象组织成测试用例、报告失败信息的框架。两者配合测试才会又快又稳。2. 为什么是 JUnit 5 配 Mockito方案选型背后的逻辑2.1 JUnit 5 到底比 JUnit 4 强在哪里很多人的第一反应是我又不是不能用 JUnit 4为什么非要升级。我当初也是这个心态直到被 JUnit 4 的扩展方式恶心到。JUnit 4 靠 RunWith 注解一个测试类只能指定一个 Runner你想同时用 Spring 的 Runner 和 Mockito 的 Runner就得自己去搞复合 Runner麻烦得要命。JUnit 5 换成了 Extension 模型支持多个扩展叠加你想用 MockitoExtension 就用 MockitoExtension想再用一个自定义扩展记录执行时间也不冲突。JUnit 5 还有一个我非常喜欢的点断言体系更加完整。assertEquals 支持 lambda 形式的消息assertThrows 直接测异常assertTimeout 可以测超时。以前 JUnit 4 遇到异常测试要写 try-catch 或者用 Test(expected...)后者会掩盖掉断言之前抛了异常的误判。在 JUnit 5 里assertThrows 能精确到异常类型还要求传入一个 Executable代码结构更清晰。另外 JUnit 5 提供了 TestInstance、ParameterizedTest、Nested 这些能力。尤其是 Nested我经常用来把同一个业务类的不同场景分组外部正常、外部异常、边界条件各放一个内部类测试代码的可读性会高出一大截。这些特性单独看是优化体验但和 Mockito 结合起来能让测试代码像一份可执行的业务文档。2.2 Mockito 的市场地位和选型理由模拟框架不止 Mockito 一个EasyMock、PowerMock 也在老项目里出现过。但我在新项目里只推荐 Mockito原因很直白API 语义最接近自然语言。when(userDao.findById(1L)).thenReturn(user)这句话读起来就是当调用 userDao.findById 并传入 1 时返回 user。而 EasyMock 的 expect 风格我在实际用的时候总觉得思维要绕一下。PowerMock 的年代感更重。它确实能 mock 静态方法和构造函数但代价是必须用自定义的类加载器去加载测试类和 JaCoCo 覆盖率插件经常冲突和 Java 9 之后的模块系统磨合得也不好。Mockito 从 3.4.0 版本开始通过 mockito-inline 也能支持静态方法、构造函数、final 方法/类而且是用字节码插桩实现的不需要额外类加载器。我实测过大部分场景下稳定性和兼容性比 PowerMock 好太多。2.3 JUnit 5 和 Mockito 的扩展模型怎么契合Mockito 官方做了 mockito-junit-jupiter 这个模块核心类是 MockitoExtension。它做的事情听起来简单但非常关键在 JUnit 5 的测试生命周期里自动识别 Mock、Spy、InjectMocks 注解把 mock 对象创建好并注入进去。你不需要在 BeforeEach 里写mock Mockito.mock(Xxx.class)也不需要手动MockitoAnnotations.openMocks(this)类一加载就给你准备好。更值得说的是它支持严格打桩Strict Stubbing。JUnit 4 时代的 Mockito 默认是宽松模式你在测试里打了一堆桩其中有一些根本没被调用测试照样通过。问题是这些无用桩会掩盖代码重构之后的行为变化你以为你还在测那个分支实际那个分支已经不存在了。MockitoExtension 默认就是严格模式一旦出现未使用的打桩它会直接抛 UnnecessaryStubbingException逼着你删掉多余的桩让测试永远保持精简。3. 环境搭建先把地基打牢3.1 Maven 依赖的完整写法与版本选择我用的组合是 JUnit 5.10.x 和 Mockito 5.x这两个版本在 Maven 中央仓库上有明确的兼容保证。Maven 的 pom 里这样配properties junit.version5.10.2/junit.version mockito.version5.11.0/mockito.version /properties dependencies dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version${junit.version}/version scopetest/scope /dependency dependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version${mockito.version}/version scopetest/scope /dependency dependency groupIdorg.mockito/groupId artifactIdmockito-junit-jupiter/artifactId version${mockito.version}/version scopetest/scope /dependency /dependencies很多人会漏掉 junit-jupiter 和 junit-jupiter-api 的区别。junit-jupiter 是聚合依赖它会把 junit-jupiter-api、junit-jupiter-engine、junit-jupiter-params 一起拉进来。如果你只引入 api跑测试的时候会得到 No tests found 的提示因为缺少 engineJUnit 平台根本不知道要执行哪个测试引擎。所以只用 junit-jupiter 这一个就够了不需要手动拆开。Mockito 5.x 默认就是内联 mock maker 吗并不是。mockito-core 默认用的是子类 mock maker能 mock 普通类、接口但不能 mock final 类和 final 方法。如果你要 mock final 类就用 mockito-inline 替换 mockito-core。注意是替换不是同时加两个否则会冲突。Gradle 用户对应这样写dependencies { testImplementation org.junit.jupiter:junit-jupiter:5.10.2 testImplementation org.mockito:mockito-core:5.11.0 testImplementation org.mockito:mockito-junit-jupiter:5.11.0 } test { useJUnitPlatform() }3.2 ExtendWith 与手动初始化的取舍在 JUnit 5 里最简单的用法是给测试类加一个注解ExtendWith(MockitoExtension.class) class UserServiceTest { Mock private UserRepository userRepository; Mock private EmailClient emailClient; InjectMocks private UserService userService; // 测试方法... }ExtendWith是 JUnit 5 的统一扩展入口。加载 MockitoExtension 后Mockito 会在每次测试执行前扫描当前测试实例里的字段看到 Mock 就创建 mock 对象看到 InjectMocks 就把已经创建好的 mock 注入进去。有些时候你不想用扩展比如测试类已经用另一个自定义扩展了或者你想在任意一个普通方法里临时 mock 一下那可以手动BeforeEach void setUp() { MockitoAnnotations.openMocks(this); }这个方法和 JUnit 5 没有任何依赖你哪怕不开 ExtendWith 也能生效。它内部库也有缓存和清理逻辑需要关注的是返回值。返回的 AutoCloseable 建议存起来在 AfterEach 里 close不然 IDE 可能会提示资源泄漏。我在真实项目里更推荐 ExtendWith因为不用自己管生命周期而且默认严格模式会自动启用的。手动 openMocks 默认不是严格模式你还要额外加 MockitoSettings 去调 strictness很别扭。3.3 第一个能跑的测试写出你的最小示例写一个简单的订单折扣计算器它依赖一个汇率服务。假设业务逻辑是根据会员等级打折再把美元价格转为人民币。我不想去真的请求汇率接口于是 mock 掉汇率服务public interface ExchangeRateService { BigDecimal convert(BigDecimal amount, String from, String to); } public class DiscountCalculator { private final ExchangeRateService exchangeRateService; public DiscountCalculator(ExchangeRateService exchangeRateService) { this.exchangeRateService exchangeRateService; } public BigDecimal calculate(BigDecimal originalAmount, int level) { BigDecimal discountRate switch (level) { case 1 - new BigDecimal(0.9); case 2 - new BigDecimal(0.8); default - BigDecimal.ONE; }; BigDecimal discounted originalAmount.multiply(discountRate); return exchangeRateService.convert(discounted, USD, CNY); } }测试类ExtendWith(MockitoExtension.class) class DiscountCalculatorTest { Mock private ExchangeRateService exchangeRateService; InjectMocks private DiscountCalculator discountCalculator; Test void should_apply_level_two_discount_and_convert_currency() { when(exchangeRateService.convert(new BigDecimal(80), USD, CNY)) .thenReturn(new BigDecimal(560)); BigDecimal result discountCalculator.calculate(new BigDecimal(100), 2); assertEquals(0, new BigDecimal(560).compareTo(result)); verify(exchangeRateService).convert(new BigDecimal(80), USD, CNY); } }注意几个点第一InjectMocks 是根据构造器注入的它会把类型匹配的 mock 传进去我们的 DiscountCalculator 只有一个 ExchangeRateService 参数所以能装配成功。第二BigDecimal 不能直接 equals 比较因为 scale 不同也会影响 equals用 compareTo 更安全。第三verify 是在验证这个方法确实被调用过、参数也对这比只断言返回值更严格。3.4 环境踩坑速查版本冲突和模块问题我遇到的第一个坑是 Java 9 之后模块系统带来的。如果项目里用了 Platform Thread 的模块路径某些 mock 内部类反射会报 InaccessibleObjectException。解决办法一般是给 JVM 加--add-opens java.base/java.langALL-UNNAMED或者在 Maven Surefire 插件配置里加上 argLine。我见过不少团队查了很久才发现是这个原因。第二个坑是引入了多个版本的 Mockito。同一个项目里 A 模块传递依赖带的是 Mockito 2.xB 模块用 Mockito 5.x结果运行时 NoSuchMethodError。遇到这种问题先去mvn dependency:tree看依赖树把老的 Mockito 用 exclude 排除掉。第三个坑最容易忽略使用 JUnit 5 后Maven 的 Surefire 插件版本太老。老版本 Surefire 根本不认识 JUnit 5 的测试类输出一堆No tests were executed!。至少用 3.0.0 以上版本我习惯直接固定 3.2.5。4. 高频模拟技巧从入门到顺手4.1 打桩到底有几种姿势什么时候用哪一种最常见的写法是when(mockFoo.bar()).thenReturn(value);这个写法有个隐含约束Mockito 会在调用 when 之前先去执行一次 mockFoo.bar()再把这次调用登记下来作为 stub。如果 bar() 返回 void这段代码就没法编译因为 when 需要参数void 方法不能被用在表达式里。所以 void 方法或者 spy 对象上的方法需要换一种姿势doReturn(value).when(mockFoo).bar(); doThrow(new RuntimeException(xxx)).when(mockFoo).bar();doReturn 系列的优点是它不要求目标方法有返回值也避免了真的去执行一次方法的副作用。特别是用 spy 的时候when(spyObj.realMethod()).thenReturn(...)会先触发真实方法执行如果真实方法有副作用写数据库、发请求测试就出问题了。这时候用 doReturn(...).when(spyObj) 才是安全的。我踩过一个印象很深的坑用 spy 监听一个缓存类然后when(cache.get(key)).thenReturn(value)结果每次跑测试真的去执行了 cache.get 里的原始代码反而抛了空指针。改成 doReturn 后问题立刻消失。经验法则就是凡是你不确定目标方法是不是安全、可重复执行的一律用 doReturn/doThrow 系列。4.2 参数匹配器别像个傻子一样只打固定值打桩里最忌讳的是把参数写死。你 mock 一个 findById(1L)测试里实际调用的是 findById(2L)那永远匹配不上。为了让测试贴近真实调用应该用参数匹配器when(userRepository.findById(anyLong())).thenReturn(optionalUser); when(userRepository.save(any(User.class))).thenAnswer(invocation - { User u invocation.getArgument(0); u.setId(100L); return u; });anyLong()、eq(value)、anyString()、argThat(自定义lambda) 是常用的几个。这里有个非常关键但不讲没人提醒的规则如果你决定使用参数匹配器那方法的所有参数都必须使用匹配器不能出现一个用匹配器、一个用真实值。// 错误写法运行时报 InvalidUseOfMatchersException when(foo.findByNameAndAge(张三, anyInt())).thenReturn(...); // 正确写法 when(foo.findByNameAndAge(eq(张三), anyInt())).thenReturn(...);为什么强制因为 Mockito 在编译期不做检查运行期靠一个内部标记ArgumentMatcherStorage来区分参数是匹配器还是真实值。混用时Mockito 不知道 张三 到底是匹配器还是真实值直接抛异常。这个错误信息特别容易让人懵我见过同事改了一上午没找到原因。argThat 和 any 的区别也值得提。any(SomeClass.class) 只判断类型argThat 可以写更复杂的逻辑比如金额必须大于 0when(paymentClient.charge(argThat(a - a.compareTo(BigDecimal.ZERO) 0))) .thenReturn(successResponse);4.3 模拟异常和 void 方法业务方法里经常有这种场景发送通知失败不能影响主流程或者外部调用抛异常需要重试。测试里这样写doThrow(new PaymentException(insufficient balance)) .when(paymentClient).charge(any(BigDecimal.class)); assertThrows(PaymentException.class, () - { orderService.createOrder(...); });重点说下 doAnswer。它像一个钩子可以在 mock 方法被调用时执行一段自定义逻辑还能拿到参数做动态返回。这比写死的 thenReturn 灵活太多。比如你有一个 ID 生成器接口你想测试每次调用生成的 ID 都不同when(idGenerator.nextId()).thenAnswer(invocation - { return counter.incrementAndGet(); });还有更实际的使用场景mock 一个消息队列发送器进去后自动给消息打上已发送标记。这种副作用 doAnswer 非常合适它可以拿到 invocation.getArgument(0)。4.4 verify不止是调用过而是按预期调用过测试里最容易写的是 when最容易忽略的是 verify。when 管的是被测方法的返回值从哪来verify 管的是某些协作对象的方法是否真的被调用、调了几次、调用顺序对不对。两者一起用才完整。常见的 verify 用法verify(userRepository, times(1)).save(user); verify(emailClient, never()).sendEmail(anyString()); verify(paymentClient, atLeastOnce()).charge(any()); verify(userRepository, atMost(3)).findById(anyLong());如果业务逻辑里有分支比如保存成功后必须发通知保存失败则不发你用 verify(never()) 就能精确测出这个行为。这比去 mock 返回 null 然后看有没有 NPE 要优雅得多。跨多个 mock 对象的时候我想验证调用顺序用 InOrderInOrder inOrder inOrder(auditLog, paymentClient); inOrder.verify(auditLog).record(START); inOrder.verify(paymentClient).charge(any()); inOrder.verify(auditLog).record(END);还有 verifyNoMoreInteractions。它要求这个 mock 的所有方法在测试里交代清楚有没验证过的调用会抛异常。我一般只在极小的接口上用它因为大接口上太严格改业务代码后测试很容易碎一地。4.5 静态方法、构造函数和 final 类现代 Mockito 的硬核能力Mockito 3.4 之前我不能碰静态方法必须用 PowerMock。现在用 mockito-inline 后代码干净很多。模拟静态方法要放在 try-with-resources 里否则会报 was not closedtry (MockedStaticIdGenerator mockedStatic mockStatic(IdGenerator.class)) { mockedStatic.when(IdGenerator::generate).thenReturn(FIXED-ID-001); String result orderService.createOrder(...); assertEquals(FIXED-ID-001, result); mockedStatic.verify(IdGenerator::generate, times(1)); }作用域的限制不是 Mockito 故意刁难人是因为静态 mock 会修改全局字节码不限制作用域的话线程里其他并发测试也会被影响。你想想两个测试同时 mock 同一个静态方法各自期待不同的返回值那不乱套才怪。try-with-resources 保证测试结束就自动还原现场。mockConstruction 的用法类似try (MockedConstructionEmailClient construction mockConstruction(EmailClient.class)) { orderService.sendEmailToUser(testexample.com); EmailClient mockedClient construction.constructed().get(0); verify(mockedClient).send(testexample.com); assertEquals(1, construction.constructed().size()); }final 类和 final 方法只要用了 mockito-inline默认就能 mock不需要额外写什么。但要注意如果你的测试目标是一个 final 类而且你还要用 InjectMocks 注入进去那注意构造器得能被 Mockito 访问不能是 private。5. 实战拆解一个订单服务测试从设计到落地5.1 先画业务用例再写测试代码空谈 API 太虚我直接用一个贴近真实业务的例子。假设有个 OrderService依赖四个东西UserRepository查用户信息PaymentClient外部支付接口NotificationClient发送通知IdGenerator静态工具类生成订单号业务规则用户不存在抛 UserNotFoundException不扣款不发通知用户存在但余额不足抛 PaymentException不发通知支付成功后必须发通知并且通知内容里包含订单号用户是 VPI 会员享受 9 折优惠为了设计测试用例我先列一个测试矩阵正常路径、用户不存在、余额不足、支付超时。这样写代码的时候思路是清晰的不会写一半发现漏分支。5.2 完整测试代码逐块讲解ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock private UserRepository userRepository; Mock private PaymentClient paymentClient; Mock private NotificationClient notificationClient; InjectMocks private OrderService orderService; Test void should_create_order_and_send_notification_when_payment_succeeds() { User user new User(1L, 张三, new BigDecimal(1000), 2); when(userRepository.findById(1L)).thenReturn(Optional.of(user)); when(paymentClient.charge(new BigDecimal(90))).thenReturn(PaymentResult.SUCCESS); try (MockedStaticIdGenerator mockedStatic mockStatic(IdGenerator.class)) { mockedStatic.when(IdGenerator::generate).thenReturn(NO-2024-001); Order order orderService.createOrder(1L, new BigDecimal(100)); assertEquals(NO-2024-001, order.getOrderNo()); assertEquals(new BigDecimal(90), order.getActualAmount()); verify(userRepository).findById(1L); verify(paymentClient).charge(new BigDecimal(90)); verify(notificationClient).send(1L, 订单 NO-2024-001 支付成功); } } Test void should_throw_when_user_not_found() { when(userRepository.findById(99L)).thenReturn(Optional.empty()); UserNotFoundException exception assertThrows(UserNotFoundException.class, () - orderService.createOrder(99L, new BigDecimal(50))); assertEquals(用户不存在, exception.getMessage()); verify(paymentClient, never()).charge(any()); verify(notificationClient, never()).send(eq(99L), anyString()); } Test void should_not_send_notification_when_payment_fails() { User user new User(1L, 李四, new BigDecimal(10), 0); when(userRepository.findById(1L)).thenReturn(Optional.of(user)); doThrow(new PaymentException(余额不足)) .when(paymentClient).charge(any(BigDecimal.class)); assertThrows(PaymentException.class, () - orderService.createOrder(1L, new BigDecimal(50))); verify(notificationClient, never()).send(anyLong(), anyString()); } }对应业务代码大概是这样public Order createOrder(Long userId, BigDecimal amount) { User user userRepository.findById(userId) .orElseThrow(() - new UserNotFoundException(用户不存在)); BigDecimal actualAmount amount; if (user.getLevel() 2) { actualAmount amount.multiply(new BigDecimal(0.9)); } String orderNo IdGenerator.generate(); try { paymentClient.charge(actualAmount); } catch (PaymentException e) { throw e; } notificationClient.send(userId, 订单 orderNo 支付成功); return new Order(orderNo, userId, actualAmount); }注意第三测试里的细节我在 paymentClient.charge 上用了 doThrow因为 charge 可能声明返回 PaymentResult也可能是 void用 doThrow 都成立。然后 verify(notificationClient, never()).send(anyLong(), anyString())这一步很关键它验证了支付失败后通知链路确实被切断。5.3 细节怎样让测试互不影响测试类里 mock 是有状态的。如果两个测试方法共享同一个 mock 实例前一个测试打桩的返回值可能会污染下一个测试。Mockito 默认每个测试方法跑完后会清理 mock 的状态。但你自己在测试方法里创建的本地 mock 或者外部静态资源一定要手动清理。静态 mock 我前面说了用 try-with-resources这是最稳妥的方案。另外 InjectMocks 的注入时机是每测试方法执行前。MockitoExtension 会为每个测试方法重新创建一个测试实例JUnit 5 默认模式字段上的 mock 也会重新生成所以天然隔离。如果你把 TestInstance(PER_CLASS) 打开了那就得注意 mock 状态不会自动重置这时候可以考虑在 BeforeEach 里显式 reset 相关 mock。5.4 测试命名和结构让测试被人看得懂我极不建议 testCreateOrder 这种命名。它只说我在测创建订单没说什么条件下、期望什么结果。我习惯用 should_do_xxx_when_yyy 这种格式翻译过来是当 yy 发生时应该做 xx。上面测试方法里的名字一长溜一开始有人觉得啰嗦但后来团队里新同学看测试的时候几乎不用再去翻业务代码测试本身就是文档。JUnit 5 的 Nested 可以更进一步我经常这样组织Nested class 支付成功 { // 正常路径测试 } Nested class 用户异常 { // 找不到用户 } Nested class 支付异常 { // 余额不足、超时 }用中文类名没问题JVM 支持测试报告里显示也清晰。6. 常见错误排查清单我来当你的排雷手册下面这张表是我这几年解决过的高频问题照着对照排查比反复试错高效得多。错误现象根本原因解决方式UnnecessaryStubbingException严格模式下存在未被使用的打桩删除无用 when 语句或者拆分测试InvalidUseOfMatchersException参数匹配器和真实值混用全参数都改成匹配器Cannot mock/spy because final class默认 MockMaker 不支持 final依赖换成 mockito-inline或在 resources 下配置 MockMakerMockitoException: was not closedmockStatic/mockConstruction 作用域未关闭把静态 mock 放进 try-with-resourcesNo tests found 或找不到 engineSurefire 版本太老或缺少 junit-jupiter-engine升级 Surefire 至 3.0.0只引入 junit-jupiter 聚合依赖InaccessibleObjectExceptionJava 9 模块对反射的限制添加 --add-opens 或调整模块描述文件测试之间相互影响、偶发失败静态状态没清理或 mock 被共享静态 mock 限定作用域检查 TestInstanceMockitoAnnotations.openMocks 后注入失败测试类私有字段或构造器不可访问改为包私有字段或给被测类提供可访问的构造器我再提两个特别隐蔽的坑。第一个在 BeforeEach 里打了桩在某个测试方法里却没用到。严格模式下这个桩会直接让测试失败提示你可能多写了一行 when。很多人刚升 Mockito 5 时会吓一跳以为是 bug。实际上这是设计好的所有桩必须是该测试真的需要的。如果确实想保留这个通用桩就在测试类上标注MockitoSettings(strictness Strictness.LENIENT)或者干脆把打桩逻辑放到具体的测试方法里。第二个mock 对象和被 mock 的对象被并发访问。JUnit 5 默认单线程执行测试但如果你配置了并行执行Mockito 的某些内部状态是非线程安全的。这时别执着于用 Mockito 设置全局配置优先把并行的测试类隔离开。我的经验是单元测试阶段没必要做真正意义的并行收益小成本高不如把测试时间花在减少启动开销上。7. 和 Spring Boot 测试搭配时的取舍很多人在 Spring Boot 项目里会问我已经用了 SpringBootTest还需要 Mockito 吗答案是看你要测什么。如果测的是 Service 层逻辑我强烈建议不要启动 Spring 上下文。你用 ExtendWith(MockitoExtension.class) 加 Mock/InjectMocks一个测试跑下来几十毫秒。而 SpringBootTest 光启动容器就要好几秒还依赖配置文件、数据库连接等一堆外部条件。我见过一个项目因为全部用 SpringBootTest全量测试跑了快二十分钟开发反馈周期长到离谱。如果是有 Controller 的 Web 层测试可以用 WebMvcTest 加 MockBean。MockBean 能把容器里某个 Bean 替换成 mock这样测试 Web 层时不用拉起整个上下文但仍然比纯 Mockito 慢适合数量不要太多的场景。我个人是这么分层的Service 层用 ExtendWithRepository 层的数据访问测试用 DataJpaTest 加真实内存库Controller 层用 WebMvcTest。集成测试需要多个服务协作、真实中间件的场景才用 SpringBootTest。这个分层让单测、组件测试、集成测试的目标清晰跑得也快。注意 MockBean 在 Spring Boot 3.4 里被标记为废弃推荐用 MockitoBean。迁移成本不高换注解就行但如果你还在老版本只要别混用同一个测试类上的不同机制就好。8. 关于测试工程师的自我修养最后多讲几句借用我团队里的一次 Code Review 经历同事提交的测试类写了五百行一半都是重复的 when 打桩我看得头皮发麻。后来建议大家用测试金字塔的思路去设计系统——尽量多写运行快的单元测试少量写需要真实组件的集成测试端到端只覆盖关键核心链路。这个思路帮我们省下了大量排队跑 CI 的时间。别过度模拟。如果你发现一个测试方法里打了几十个桩那大概率不是测试的问题而是被测类的设计耦合太重。我在重构时经常遇到这种情况一个 Service 里 new 了太多依赖或者依赖了太多静态方法导致测试不得不把所有东西都 mock 掉。这时候优先考虑依赖注入、抽取接口而不是勉强写一个复杂的测试。关于模拟对象和真对象的边界我的习惯是值对象VO、DTO、PO用真的因为它们没有逻辑用真对象能让断言更直观服务、仓储、外部客户端用 mock因为这些才是需要交互控制的点。模拟一个纯数据类除了让代码变啰嗦没有任何额外收益。最后一个技巧写完测试后我总会做一个故障注入练习——把业务代码里的关键逻辑故意改坏比如把比较符号从 改成 然后跑一遍测试看看是不是真的会红。如果没红说明测试没覆盖到这个逻辑我会补上对应断言。这个方法成本极低却能极大地提高测试的有效性推荐每个人都试一试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑