Mockito 进阶实战——静态方法 Mock、私有方法、构造函数 Mock
Mockito 进阶实战——静态方法 Mock、私有方法、构造函数 Mock
适读人群:Mockito 基础已掌握,遇到过"静态方法没法 mock"、"私有方法测不到"等困境的工程师 | 阅读时长:约13分钟 | 核心价值:彻底解锁 Mockito 进阶能力,不再被"不可测代码"挡住
那堆"没法测"的遗留代码
接手遗留项目时,最让我头疼的不是业务逻辑多复杂,而是代码里到处都是这样的东西:
public class OrderProcessor {
public ProcessResult process(OrderRequest request) {
// 调用静态工具类
String traceId = TraceUtils.generateTraceId();
LogUtils.info("开始处理订单: " + traceId);
// 直接 new 对象(硬编码依赖)
ExternalPaymentClient client = new ExternalPaymentClient(
ConfigUtils.getProperty("payment.host"),
ConfigUtils.getProperty("payment.key")
);
// 调用私有方法
if (!validateRequest(request)) {
return ProcessResult.failed("请求验证失败");
}
return client.charge(request.getAmount());
}
private boolean validateRequest(OrderRequest request) {
// 复杂的验证逻辑
return request != null && request.getAmount() > 0;
}
}这段代码,在传统 Mockito(3.x 以下)里几乎没法写单元测试:
TraceUtils.generateTraceId()是静态方法new ExternalPaymentClient(...)是直接构造依赖validateRequest是私有方法
很多工程师遇到这种情况就放弃了——"这代码没法测,不管了"。
但从 Mockito 3.4.0 开始,配合 mockito-inline,这些问题都能解决。
静态方法 Mock:mockStatic()
准备工作
首先需要添加 mockito-inline 依赖(Mockito 5.x 已内置,不需要额外引入):
<!-- Mockito 4.x 及以下需要 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>Mockito 5.x:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>基本用法
@Test
void testGenerateTraceId_mock() {
// mockStatic 必须在 try-with-resources 里,确保使用后关闭
try (MockedStatic<TraceUtils> mockedTraceUtils = mockStatic(TraceUtils.class)) {
// 设定静态方法的返回值
mockedTraceUtils.when(TraceUtils::generateTraceId)
.thenReturn("fixed-trace-id-for-test");
// 调用被测方法
String traceId = TraceUtils.generateTraceId();
assertEquals("fixed-trace-id-for-test", traceId);
// 验证静态方法被调用了
mockedTraceUtils.verify(TraceUtils::generateTraceId, times(1));
}
// try 块结束后,mock 自动关闭,恢复真实实现
}带参数的静态方法
@Test
void testOrderProcessor_withStaticMock() {
try (MockedStatic<TraceUtils> mockedTrace = mockStatic(TraceUtils.class);
MockedStatic<ConfigUtils> mockedConfig = mockStatic(ConfigUtils.class)) {
mockedTrace.when(TraceUtils::generateTraceId).thenReturn("test-trace-001");
mockedConfig.when(() -> ConfigUtils.getProperty("payment.host")).thenReturn("localhost");
mockedConfig.when(() -> ConfigUtils.getProperty("payment.key")).thenReturn("test-key");
OrderProcessor processor = new OrderProcessor();
// 现在可以测试了,ConfigUtils 会返回 mock 值
// ...
}
}静态方法的 ArgumentMatchers
try (MockedStatic<StringUtils> mockedUtils = mockStatic(StringUtils.class)) {
// 匹配任意字符串参数
mockedUtils.when(() -> StringUtils.isBlank(anyString())).thenReturn(false);
// 匹配特定参数
mockedUtils.when(() -> StringUtils.isBlank("")).thenReturn(true);
mockedUtils.when(() -> StringUtils.isBlank(null)).thenReturn(true);
}构造函数 Mock:mockConstruction()
这解决了"直接 new 对象"导致无法替换依赖的问题。
@Test
void testOrderProcessor_mockConstruction() {
// 当代码里 new ExternalPaymentClient(...) 时,返回 mock 对象
try (MockedConstruction<ExternalPaymentClient> mockedClient =
mockConstruction(ExternalPaymentClient.class,
(mock, context) -> {
// context 包含构造函数的参数
System.out.println("构造函数被调用,参数: " + context.arguments());
// 设定 mock 行为
when(mock.charge(anyBigDecimal()))
.thenReturn(PaymentResult.success("txn-mock-001"));
})) {
OrderProcessor processor = new OrderProcessor();
ProcessResult result = processor.process(buildValidRequest());
// 验证结果
assertTrue(result.isSuccess());
// 验证 ExternalPaymentClient 被构造了
assertEquals(1, mockedClient.constructed().size());
// 验证 charge 被调用了
ExternalPaymentClient constructedClient = mockedClient.constructed().get(0);
verify(constructedClient).charge(any());
}
}验证构造函数参数
try (MockedConstruction<ExternalPaymentClient> mockedClient =
mockConstruction(ExternalPaymentClient.class,
(mock, context) -> {
// 验证构造时传入了正确的参数
List<?> args = context.arguments();
assertEquals("payment.example.com", args.get(0));
assertNotNull(args.get(1)); // API key 不为空
})) {
new OrderProcessor().process(buildValidRequest());
}私有方法测试:要不要 Mock?
这是个有争议的话题,我明确表态:私有方法原则上不应该直接测试,但有时候绕不开。
正确姿势一:不直接测,通过公开方法间接测
// 私有方法
private boolean validateRequest(OrderRequest request) {
return request != null
&& request.getAmount() > 0
&& request.getUserId() != null;
}
// 不直接测 validateRequest,而是测 process 的各种 case
@Test
void testProcess_whenRequestIsNull_shouldFail() {
ProcessResult result = processor.process(null);
assertFalse(result.isSuccess());
assertEquals("请求验证失败", result.getMessage());
}
@Test
void testProcess_whenAmountIsNegative_shouldFail() {
ProcessResult result = processor.process(new OrderRequest(null, BigDecimal.valueOf(-1)));
assertFalse(result.isSuccess());
}这种方式更合理——私有方法是实现细节,测试应该关注行为(公开方法),不关注实现(私有方法)。
正确姿势二:抽取成独立类
如果私有方法太复杂,值得单独测试,那说明它可能应该是一个独立的类或工具方法:
// 重构前:OrderProcessor 里的私有方法
private boolean validateRequest(OrderRequest request) { ... }
// 重构后:独立成类,可以直接测
public class OrderRequestValidator {
public ValidationResult validate(OrderRequest request) { ... }
}非要测私有方法:反射或 PowerMock
如果是遗留代码,确实需要直接测私有方法:
@Test
void testValidateRequest_viaReflection() throws Exception {
OrderProcessor processor = new OrderProcessor();
// 通过反射访问私有方法
Method validateMethod = OrderProcessor.class
.getDeclaredMethod("validateRequest", OrderRequest.class);
validateMethod.setAccessible(true);
// 测试 null 请求
boolean result = (boolean) validateMethod.invoke(processor, (Object) null);
assertFalse(result);
// 测试金额为负
OrderRequest invalidRequest = new OrderRequest(1L, BigDecimal.valueOf(-1));
result = (boolean) validateMethod.invoke(processor, invalidRequest);
assertFalse(result);
// 测试正常请求
OrderRequest validRequest = new OrderRequest(1L, BigDecimal.valueOf(100));
result = (boolean) validateMethod.invoke(processor, validRequest);
assertTrue(result);
}踩坑实录三则
踩坑一:mockStatic 忘了用 try-with-resources,影响其他测试
现象:一个测试文件里,某个测试 mock 了 System.currentTimeMillis(),结果后面所有测试的时间相关逻辑都乱了。
原因:MockedStatic 如果不关闭,会一直生效,影响同线程的所有后续代码。
解法:必须用 try-with-resources,或者在 @AfterEach 里手动 close()。
private MockedStatic<TraceUtils> mockedTrace;
@BeforeEach
void setUp() {
mockedTrace = mockStatic(TraceUtils.class);
}
@AfterEach
void tearDown() {
mockedTrace.close(); // 必须关闭!
}推荐 try-with-resources,更安全,不会因为 @AfterEach 里的其他代码抛异常而跳过 close。
踩坑二:mockConstruction 里的 mock 行为设定时机问题
现象:
try (MockedConstruction<Foo> mocked = mockConstruction(Foo.class)) {
// 试图在外面设定 mock 行为
Foo constructedFoo = mocked.constructed().get(0); // IndexOutOfBoundsException!
when(constructedFoo.doSomething()).thenReturn("test");
}原因:mocked.constructed() 是在对象被构造之后才有内容的。mock 行为应该在构造时(通过第二个参数的 lambda)设定,而不是事后。
解法:
try (MockedConstruction<Foo> mocked = mockConstruction(Foo.class,
(mock, context) -> {
// 正确:在构造时设定行为
when(mock.doSomething()).thenReturn("test");
})) {
// 现在调用被测代码,Foo 会在内部被构造
service.doWork(); // service 内部会 new Foo()
// 构造完成后可以获取并验证
assertEquals(1, mocked.constructed().size());
verify(mocked.constructed().get(0)).doSomething();
}踩坑三:反射调用私有方法时,异常被包装成 InvocationTargetException
现象:被测的私有方法抛了 IllegalArgumentException,但 JUnit 捕获到的是 InvocationTargetException,无法用 assertThrows 正常匹配。
原因:反射调用时,目标方法抛出的异常会被包装在 InvocationTargetException.getCause() 里。
解法:
@Test
void testPrivateMethod_throwsException() throws Exception {
Method method = SomeClass.class.getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
try {
method.invoke(instance, "invalid_input");
fail("应该抛出异常");
} catch (InvocationTargetException e) {
// 真正的异常在 getCause() 里
assertInstanceOf(IllegalArgumentException.class, e.getCause());
assertEquals("输入不合法", e.getCause().getMessage());
}
}什么时候不该用静态 Mock
用 mockStatic 和 mockConstruction 能解决测试难题,但也要警惕滥用。
如果你发现代码里到处是 mockStatic(DateUtils.class) 或 mockStatic(UUIDUtils.class),说明这些工具类的依赖没有通过接口注入,是硬编码依赖。更好的做法是把这些依赖抽象成接口:
// 不好:直接调用静态方法
public class OrderService {
public Order create(OrderRequest request) {
String orderId = UUIDUtils.generate(); // 静态依赖,难测
// ...
}
}
// 好:通过接口注入
public class OrderService {
private final IdGenerator idGenerator; // 接口
public OrderService(IdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
public Order create(OrderRequest request) {
String orderId = idGenerator.generate(); // 可注入、可 mock
}
}
// 测试时注入 mock
@Test
void testCreate() {
IdGenerator mockIdGen = mock(IdGenerator.class);
when(mockIdGen.generate()).thenReturn("test-id-001");
OrderService service = new OrderService(mockIdGen);
// ...
}mockStatic 是对付遗留代码的工具,不是新代码的设计方向。能用依赖注入解决的,就不要用 mockStatic。
