
简介面向Java开发者的单元测试工具包整合JUnit 4.12与Mockito两个主流框架解决模块验证和依赖隔离两大测试痛点。JUnit 4.12提供注解、参数化测试、测试规则等能力配合断言可以快速检查方法输出是否符合预期Mockito则通过Mock、when()、thenReturn()、verify()等API创建模拟对象替代真实依赖确保测试在隔离环境中稳定运行。压缩包大小约3.55MB内含JUnit 4.12.jar、Mockito.jar以及Mockito使用文档文档覆盖API参考、配置示例与最佳实践既适合零基础开发者逐步上手也可作为团队内部测试培训的参考资料。对于需要维护遗留代码或重构的工程师这套组合能有效搭建回归测试基础尽早暴露接口变更带来的影响减少对外部数据库、网络服务的耦合依赖同时也能帮助新手建立对测试替身、行为验证等概念的实际认知。目前已有618人学习下载特别适合希望建立规范化单元测试体系的初中级Java工程师。 干这一行久了你会发现单元测试这事儿技术难度不高但能写出真正有质量、能扛住回归的单测靠的是经验和对框架细节的把握。我在存量项目里补测试、给团队做代码质量治理时用得最顺手、也最稳的一套组合就是JUnit 4.12 Mockito。JUnit 4.12 是 JUnit 4 系列里公认最稳的版本Mockito 则是把“外部依赖”这块最难啃的骨头啃下来的利器——当被测类里面塞了数据库操作、远程调用、缓存服务靠 Mockito mock 掉它们单测才能真正聚焦到业务逻辑本身。这篇文章不打算从零教你怎么写Test而是把我在实际项目里用这套组合踩过的坑、验证过的用法、以及沉淀下来的套路一次讲清楚适合正在维护老项目、准备给存量代码补单测的人也适合刚接触 Mockito 的新手直接照着用。1. 为什么还在用 JUnit 4.12 Mockito一组老而稳的组合1.1 版本选型背后的考量很多新项目已经上了 JUnit 5但现实是大量线上跑着的业务系统仍然是 Spring Boot 1.x/2.x JUnit 4 的技术栈。JUnit 4.12 是 JUnit 4 系列里最晚的稳定版本修复了 4.11 之前的一批已知问题而且和spring-test、maven-surefire-plugin的兼容性非常成熟。对存量项目来说贸然升级 JUnit 5 意味着要重写所有测试类的注解导入、调整 Runner 机制风险和收益完全不成正比。Mockito 的版本选择反而更需要注意。如果你在 Maven 里搜mockito-all会看到 1.10.19 这个很早的版本很多老教程喜欢用它但我强烈建议不要碰。mockito-all是把 CGLIB、Objenesis 等依赖全部打在一起的 fat jar版本老旧且容易出现类冲突。正确的做法是用mockito-core针对 JUnit 4.12我推荐 2.x 系列比如 2.28.2。这个版本天然兼容 Java 7/8注解 API 和 1.x 基本一致但又修复了 1.x 里很多边界情况下的 bug。提示如果你的项目 JDK 版本在 8 以下不要用 Mockito 3.x3.x 强制要求 Java 8。老项目老老实实用 2.x功能上完全够用。1.2 这套组合能解决什么问题单元测试的目标是验证“被测类自身的逻辑”而不是验证它的协作者。一个典型场景UserService依赖UserDao做数据库操作你测register()方法时总不能真的连数据库。Mockito 做的事就是“造一个假的 UserDao 替身”你告诉它“当调用findByEmail()时返回某个对象”然后UserService在这个受控的替身下运行你就能精确判断业务逻辑对不对。这个“替身演员”的思路可以解决三类问题依赖不可控数据库数据、网络结果、时间函数这些在测试环境里都是变量mock 掉之后变量变成常量。依赖太重连接数据库、读取文件、初始化 Spring 容器都会拖慢测试速度mock 掉之后测试变轻。异常难触发比如连接超时、数据库宕机这类异常mock 可以轻松模拟真实环境里反而很难复现。JUnit 负责“组织测试用例、断言结果、统计通过失败”Mockito 负责“构造替身、定义行为、验证交互”。两者各管一块配合起来刚好覆盖单测的全部诉求。2. 环境搭建与第一个单测从依赖到跑通2.1 Maven 依赖配置与版本匹配以 Maven 为例直接在pom.xml中加入以下依赖dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.12/version scopetest/scope /dependency dependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version2.28.2/version scopetest/scope /dependency这里有个经验JUnit 和 Mockito 的 scope 必须设为test避免测试框架被带入生产依赖。另外如果你的项目里已经有spring-boot-starter-test它默认集成了 JUnit 4.12 和 Mockito 2.x不需要重复引入但要注意版本冲突时以显式声明的为准。2.2 第一个 Mockito 单测的完整示例我们用一个最典型的“注册用户”业务来演示。UserService注册用户前需要检查邮箱是否重复这个检查依赖UserDaopublic class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } public void register(String email) { User existed userDao.findByEmail(email); if (existed ! null) { throw new IllegalArgumentException(邮箱已存在 email); } userDao.save(new User(email)); } }对应的测试类import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class UserServiceTest { Mock private UserDao userDao; InjectMocks private UserService userService; Before public void setUp() { MockitoAnnotations.initMocks(this); } Test public void testRegister_duplicateEmail_shouldThrow() { User existed new User(testexample.com); when(userDao.findByEmail(testexample.com)).thenReturn(existed); try { userService.register(testexample.com); fail(应该抛出异常); } catch (IllegalArgumentException e) { assertEquals(邮箱已存在testexample.com, e.getMessage()); } verify(userDao, never()).save(any(User.class)); } Test public void testRegister_newEmail_shouldSave() { when(userDao.findByEmail(newexample.com)).thenReturn(null); userService.register(newexample.com); verify(userDao).save(any(User.class)); } }注意两个细节。第一setUp()里用MockitoAnnotations.initMocks(this)初始化Mock和InjectMocks注解这是最稳妥的方式虽然有MockitoJUnitRunner可以替代但 Runner 会限制测试类的扩展比如和 Spring 的 Runner 冲突遇到复杂场景时还是initMocks更灵活。第二verify(userDao, never()).save(...)这个验证非常关键它确认了“邮箱重复时不应该触发保存”这比只断言异常更有说服力。提示Mockito 对错误消息的断言可以用Rule public ExpectedException thrown ExpectedException.none()但在 JUnit 4.12 里这个规则有一个已知的坑如果测试方法里既有thrown.expect()又有其他断言异常的行号报告会不准确。我更推荐 try-catch fail 的方式逻辑更直观。3. 核心 API 实操把 Mockito 用顺手的关键细节3.1 打桩Stubbingwhen...thenReturn 与 doReturn...when打桩是 Mockito 最核心的操作意思是“当对象的方法被调用时返回什么结果”。最常用的写法是when(userDao.findByEmail(testexample.com)).thenReturn(existed);这行代码的读法是当findByEmail收到参数testexample.com时返回existed对象。它的原理是“先调用真实方法再记录返回值设定”所以当方法本身有副作用时会先执行一次真实调用。如果方法返回void或者调用会抛异常就需要换一种写法doThrow(new RuntimeException(数据库连接失败)).when(userDao).save(any(User.class)); doReturn(existed).when(userDao).findByEmail(testexample.com);doReturn().when()和when().thenReturn()的另一个区别是类型检查。when().thenReturn()在编译期会检查返回类型类型不匹配直接编译失败doReturn().when()不做编译期检查运行时才暴露问题写起来更灵活但容易埋坑。我的原则是能不用doReturn就不用只有遇到 spy 对象或方法有副作用时才切过去。用any()、anyString()、eq()这类参数匹配器时要特别注意“ALL-OR-NOTHING”原则。如果你在打桩时用了匹配器那么所有参数都必须用匹配器不能混着写。下面的代码就是一个经典的反例// 错误示例第一个参数用匹配器第二个参数用原值运行时会抛 InvalidUseOfMatchersException when(userDao.findByNameAndAge(test, anyInt())).thenReturn(user);正确写法是when(userDao.findByNameAndAge(eq(test), anyInt())).thenReturn(user)。3.2 行为验证verify 与参数匹配器Mockito 和 Easymock 最大的不同是它鼓励“先打桩、后验证”。验证的核心 API 是verify用来确认 mock 对象上的某个方法是否被调用、调用了多少次。常用的验证场景verify(userDao).save(any(User.class)); // 默认 times(1) verify(userDao, times(3)).findByEmail(anyString()); // 调用 3 次 verify(userDao, never()).delete(any(User.class)); // 从未调用 verify(userDao, atLeastOnce()).update(any(User.class)); // 至少调用 1 次 verify(userDao, atMost(2)).queryAll(); // 最多调用 2 次verify做的是“行为确认”这和 JUnit 的assertEquals做“状态确认”是互补关系。状态确认回答“结果对不对”行为确认回答“过程对不对”。在一个注册接口里即使save方法没有返回结果我们也能通过verify确保它被调用了这在状态断言无法覆盖 void 方法时特别有用。verifyNoMoreInteractions这个 API 我建议谨慎使用。它的意思是“确认 mock 对象上所有交互都已经被 verify 覆盖”一旦被测类后续加了一个新依赖调用测试就会失败。在维护老项目时这种过度约束会让单测变得脆弱我通常只在“严格验证时序”的关键测试里使用。3.3 Mock、InjectMocks 注解与测试隔离注解的作用是简化 mock 对象的声明和注入。Mock创建一个 mock 实例InjectMocks则把 mock 注入到被测类中。InjectMocks的注入顺序是构造函数注入优先其次 setter 注入最后字段注入。这一点必须记清楚否则你会遇到“mock 没注入进去测试跑出来全是空指针”的诡异问题。来看一个实际案例Mock private UserDao userDao; Mock private MailService mailService; InjectMocks private UserService userService; // UserService 构造需要 userDao 和 mailServiceMockito 会通过构造器反射找到UserService(UserDao, MailService)然后把两个 mock 按类型匹配放进去。如果你的被测类有多个构造函数Mockito 会选择参数最多的那个构造器。想象一下你写了一个测试想用无参构造结果 Mockito 因为某个测试里多声明了一个Mock自动选择了另一个构造器后续的行为就全乱了。遇到这种情况我建议直接把InjectMocks换成手动构造Before public void setUp() { userService new UserService(userDao, mailService); }手动构造虽然代码多一点但注入关系一目了然排查问题的时候节省的时间远超这几行代码。测试隔离是一个容易被忽略的重点。JUnit 默认每个测试方法都会创建一个新的测试类实例所以Mock对象在不同测试方法之间是隔离的。但如果你在BeforeClass或 static 变量里存了 mock 或数据那这些状态会在多个测试方法之间共享很容易串数据。我见过一个项目的测试BeforeClass里初始化了一个静态 map结果多个测试类跑的时候互相污染排错排了一整天。记住mock 对象和桩数据的状态范围只应该限定在单个测试方法内。4. 常见问题与排查技巧实录4.1 mock 不了静态方法和私有方法这是 Mockito 2.x 最常被吐槽的限制。Mockito 通过动态代理实现 mock而静态方法和私有方法是基于类的元信息调用的不走实例代理所以默认无法被 mock。遇到静态方法依赖时有两条路一是重构代码把静态方法封装成一个可注入的组件。比如你依赖IdGenerator.generate()这种静态工具方法就把它包装成IdGeneratorService接口注入到被测类中。这是最干净的做法。二是引入 PowerMock 或 Mockito 的 inline mock maker。PowerMock 和 JUnit 4.12 配合需要额外加RunWith(PowerMockRunner.class)和一个PrepareForTest注解写起来很繁琐。Mockito 3.4 之后官方支持了 inline mock maker可以 mock 静态方法但前面说过版本要求 Java 8老项目不一定满足。我的经验是静态方法依赖是代码坏味道的信号尽量通过重构解决而不是在测试框架层面硬刚。4.2 final 类与 final 方法的限制2.x 默认的 mock maker 基于 CGLIBCGLIB 通过生成子类来创建 mock所以无法 mock final 类和 final 方法。如果你的被测类依赖了一个 final 类会得到类似Cannot mock/spy because of final class的报错。解决方法有两种。一种是在src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker文件中写入如下内容启用 inline mock makermock-maker-inline这个配置让 Mockito 改用 Byte Buddy 的 instrumentation 机制可以 mock final 类。另一种是升级到 Mockito 3.x自带的 inline mock maker 默认开启。但要注意inline mock maker 在 Java 8 的某些版本上对java.lang类有限制遇到Unable to make field accessible这类报错时还是建议走重构路线。4.3 thenReturn 与 doReturn 的区别什么时候必须用 doReturnwhen().thenReturn()的底层逻辑需要“先调用方法再存桩”。当方法本身有副作用或会抛异常时这种写法就不安全了。典型场景是 spy 对象User user new User(testexample.com); UserService spyService spy(userService); // 下面这行会先执行真实的 getUser 方法如果方法里抛异常测试直接失败 when(spyService.getUser()).thenReturn(user); // 推荐不执行真实方法直接存桩 doReturn(user).when(spyService).getUser();doReturn的另一大价值是处理 void 方法。when(mock.voidMethod()).thenReturn(...)本身就是非法写法因为 void 方法没有返回值编译都过不了只能写成doNothing().when(mock).voidMethod()或doThrow(new RuntimeException()).when(mock).voidMethod()。我整理了一个简单的对比表场景when().thenReturn()doReturn().when()普通有返回值方法推荐可用void 方法不可用必须用spy 对象上的方法不推荐推荐方法本身会抛异常不推荐推荐编译期类型检查有无4.4 verify 失败、数据串台等典型坑verify失败最常见的报错是Wanted but not invoked意思是你希望的调用没有发生。排查思路先看被测逻辑里是否真的调用了这个方法再看参数是否匹配。参数不匹配是最隐蔽的被测代码传的是new User(a)你 verify 里写的是any(User.class)没问题但你 verify 里写的是eq(userA)而User类没有重写equals方法那就会失败。解决办法是给领域模型重写equals/hashCode或者改用ArgumentCaptor捕获实际参数后逐字段断言ArgumentCaptorUser captor ArgumentCaptor.forClass(User.class); verify(userDao).save(captor.capture()); User saved captor.getValue(); assertEquals(testexample.com, saved.getEmail());还有一个高频坑叫InvalidUseOfMatchersException。这个异常几乎都是因为打桩或 verify 时参数匹配器使用不一致造成的比如混用原值和匹配器。解决办法很固定所有参数都用匹配器或者干脆都用原值。测试数据串台的问题前面提过这里再补充一个容易被忽略的场景在循环里创建 mock 并打桩如果桩数据是共享静态变量第一次循环的桩可能被第二次覆盖。所以循环测试时最好每次迭代都重新创建 mock或者用Before初始化。5. 给存量项目补单测的顺序建议如果你面对的是一座“一座测试都没有”的存量代码山直接给所有类补测试会非常痛苦。我的做法是先按三个维度评估优先级类是否包含核心业务逻辑、是否频繁变更、是否被多个上层模块依赖。优先给“核心业务且高频变更且被多处依赖”的类补测试收益最大。补测试时尽量先写“行为验证型”的用例也就是先用 mock 把所有外部依赖替换掉验证核心逻辑的输入输出关系。这个阶段不需要追求覆盖率先把最危险的路径比如金额计算、状态流转、异常分支守住。等核心路径稳定了再逐步补充边界条件、异常分支和性能敏感场景。另外Mockito 的Mock粒度也需要刻意控制。mock 掉所有依赖看起来很方便但当你把 8 个依赖全部 mock 掉、打了 20 个桩时单测已经不是一个“单元”测试了而是“把代码逻辑复制一遍”的测试维护成本极高。合适的状态是被测类的直接依赖里只有会产生不确定性或性能损耗的部分需要 mock普通的数据对象、纯粹的业务对象直接用真实实例。注意mock 粒度太粗测试会退化成纯打桩练习失去保护业务逻辑的意义mock 粒度太细则会让测试行为与生产行为脱节。判断标准很简单这个 mock 换成一个真实实现后测试还能不能跑如果换了真实实现就跑不了比如连数据库那 mock 是必要的如果换了真实实现照样能跑那 mock 就是多余的。实测下来我这套 JUnit 4.12 Mockito 的组合最稳定的场景就是“给老项目补测试”。每次重构一个方法前先用这套组合把当前行为用测试固化下来重构完成后跑一遍全部测试逻辑对没对、行为变没变一目了然。Mockito 的 verify 在重构时尤其好使它能精确告诉你这个方法的副作用有没有被其他环节误删。最后分享一个我自己的习惯每个测试方法只验证一个核心行为要么是“正常路径”要么是“异常路径”要么是“边界条件”坚决不混写。如果被测类的方法体里有多个分支就一个分支一个测试方法方法名直接写成testXxx_条件_期望结果。这套命名规范看着繁琐但当你三个月后回来改代码看到测试方法名就知道它守护的是什么业务规则省下的时间远超写测试的时间。希望这篇基于实际踩坑总结出来的文章能让你在补单测的路上走得顺一点。本文还有配套的精品资源点击获取