1. MockedStatic 的深度应用
MockedStatic 高级用法演示,底层原理通过字节码操作替换静态方法实现:
java
@RunWith(MockitoJUnitRunner.class)
public class AdvancedMockStaticTest {
private MockedStatic<AndroidSchedulers> mockedAndroidSchedulers;
private MockedStatic<CustomSDK> mockedCustomSDK;
private MockedStatic<CustomLog> mockedCustomLog;
@Before
public void setUp() {
// 【功能】Mock 多个静态类
// 【底层原理】Mockito 为每个静态类创建独立的代理
mockedAndroidSchedulers = mockStatic(AndroidSchedulers.class);
mockedCustomSDK = mockStatic(CustomSDK.class);
mockedCustomLog = mockStatic(CustomLog.class);
// 【功能】设置静态方法的复杂行为
// 【底层原理】支持链式调用和条件返回
mockedAndroidSchedulers.when(AndroidSchedulers::mainThread)
.thenReturn(Schedulers.trampoline());
// 【功能】Mock 静态方法的不同调用场景
mockedCustomSDK.when(() -> CustomSDK.getInstance())
.thenReturn(mock(CustomSDK.class));
// 【功能】Mock 静态 void 方法
mockedCustomLog.when(() -> CustomLog.d(anyString(), anyString()))
.then(invocation -> {
// 【功能】自定义静态方法行为
String tag = invocation.getArgument(0);
String message = invocation.getArgument(1);
System.out.println("[" + tag + "] " + message);
return null;
});
}
@After
public void tearDown() {
// 【功能】清理静态 Mock,避免内存泄漏
// 【底层原理】释放字节码操作产生的资源
if (mockedAndroidSchedulers != null) {
mockedAndroidSchedulers.close();
}
if (mockedCustomSDK != null) {
mockedCustomSDK.close();
}
if (mockedCustomLog != null) {
mockedCustomLog.close();
}
}
@Test
public void testComplexStaticInteraction() {
// 【功能】测试复杂的静态方法交互
CustomSDK sdk = CustomSDK.getInstance();
assertNotNull(sdk);
// 【功能】验证静态方法调用
mockedCustomSDK.verify(() -> CustomSDK.getInstance());
// 【功能】验证静态方法调用次数
mockedCustomLog.verify(() -> CustomLog.d(eq("TEST"), anyString()), times(0));
}
}
- MockedStatic<AndroidSchedulers>:Mockito 用于 静态方法 Mock 的核心对象。
- mockStatic(SomeClass.class):是创建
MockedStatic<SomeClass>
对象的工厂方法,返回一个MockedStatic<SomeClass>
实例,用于后续配置和验证。诉 Mockito:"我要拦截这个类(SomeClass
)的所有静态方法调用。没有这一步,静态方法始终是"真调用",无法被替换。 - 良好的单测习惯:始终在
@After
中清理所有静态 Mock。静态 Mock 是全局的,一旦不释放会污染全局状态。避免静态 Mock 泄漏到其他测试用例,造成莫名其妙的测试失败。
2. ArgumentCaptor 高级用法
捕获 被 Mock 对象方法调用时传入的参数 ,用于断言对象属性或验证业务逻辑。适合 复杂对象、集合、Map 或多次调用 的参数检查。
java
public class AdvancedArgumentCaptorTest {
@Mock
private UserRepository mockUserRepository;
@Mock
private EmailService mockEmailService;
@Captor
private ArgumentCaptor<User> userCaptor;
@Captor
private ArgumentCaptor<List<String>> emailListCaptor;
@Captor
private ArgumentCaptor<Map<String, Object>> metadataCaptor;
private UserService userService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
userService = new UserService(mockUserRepository, mockEmailService);
}
@Test
public void testComplexArgumentCapture() {
// Arrange
String username = "testuser";
String email = "test@example.com";
List<String> roles = Arrays.asList("USER", "ADMIN");
// Act
userService.createUserWithRoles(username, email, roles);
// Assert - 捕获复杂对象
verify(mockUserRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
// 【功能】验证捕获对象的属性
assertEquals(username, capturedUser.getUsername());
assertEquals(email, capturedUser.getEmail());
assertNotNull(capturedUser.getCreatedAt());
// 【功能】捕获集合参数
verify(mockEmailService).sendNotificationEmails(emailListCaptor.capture());
List<String> capturedEmails = emailListCaptor.getValue();
assertTrue(capturedEmails.contains(email));
// 【功能】捕获 Map 参数
verify(mockEmailService).logActivity(metadataCaptor.capture());
Map<String, Object> capturedMetadata = metadataCaptor.getValue();
assertEquals("USER_CREATION", capturedMetadata.get("action"));
assertEquals(username, capturedMetadata.get("username"));
}
@Test
public void testMultipleArgumentCapture() {
// 【功能】捕获多次方法调用的参数
userService.createUser("user1", "user1@example.com");
userService.createUser("user2", "user2@example.com");
userService.createUser("user3", "user3@example.com");
// 【功能】验证多次调用
verify(mockUserRepository, times(3)).save(userCaptor.capture());
// 【功能】获取所有捕获的参数
List<User> allCapturedUsers = userCaptor.getAllValues();
assertEquals(3, allCapturedUsers.size());
// 【功能】验证每个捕获的参数
assertEquals("user1", allCapturedUsers.get(0).getUsername());
assertEquals("user2", allCapturedUsers.get(1).getUsername());
assertEquals("user3", allCapturedUsers.get(2).getUsername());
}
}
特性:
-
声明与注入
java@Captor private ArgumentCaptor<User> userCaptor; MockitoAnnotations.initMocks(this);
@Captor
简化ArgumentCaptor.forClass(User.class)
。- 避免手动构造,提高可读性。
-
单次捕获
javaverify(mockUserRepository).save(userCaptor.capture()); User capturedUser = userCaptor.getValue(); assertEquals("testuser", capturedUser.getUsername());
- 精准断言传入参数对象的属性。
-
捕获集合/Map
javaverify(mockEmailService).sendNotificationEmails(emailListCaptor.capture()); assertTrue(emailListCaptor.getValue().contains("test@example.com"));
- 检查集合内容、Map 键值,适合多维数据验证。
-
多次调用捕获
javaverify(mockUserRepository, times(3)).save(userCaptor.capture()); List<User> users = userCaptor.getAllValues(); assertEquals("user1", users.get(0).getUsername());
- 可断言调用次数与每次参数。
3. 自定义 Answer 和 Matcher
-
Answer :基于入参/状态的动态桩与副作用;保持轻量、可预测。
-
Matcher :表达复杂入参约束的布尔谓词;判空先行、避免副作用。
-
固定返回→
thenReturn
;复杂断言→Captor ;复杂匹配→Matcher ;动态逻辑/副作用→Answer。
java
public class CustomAnswerMatcherTest {
@Mock
private DatabaseService mockDatabaseService;
@Test
public void testCustomAnswer() {
// 【功能】自定义 Answer 实现复杂逻辑
when(mockDatabaseService.query(anyString())).thenAnswer(new Answer<List<User>>() {
@Override
public List<User> answer(InvocationOnMock invocation) throws Throwable {
// 【功能】根据参数动态返回结果
String sql = invocation.getArgument(0);
if (sql.contains("admin")) {
return Arrays.asList(new User("admin", "admin@example.com"));
} else if (sql.contains("user")) {
return Arrays.asList(
new User("user1", "user1@example.com"),
new User("user2", "user2@example.com")
);
}
return Collections.emptyList();
}
});
// 测试不同的查询
List<User> adminUsers = mockDatabaseService.query("SELECT * FROM users WHERE role = 'admin'");
assertEquals(1, adminUsers.size());
assertEquals("admin", adminUsers.get(0).getUsername());
List<User> regularUsers = mockDatabaseService.query("SELECT * FROM users WHERE role = 'user'");
assertEquals(2, regularUsers.size());
}
@Test
public void testCustomMatcher() {
// 【功能】自定义 Matcher 进行复杂匹配
when(mockDatabaseService.save(argThat(new ArgumentMatcher<User>() {
@Override
public boolean matches(User user) {
// 【功能】自定义匹配逻辑
return user != null &&
user.getEmail() != null &&
user.getEmail().contains("@") &&
user.getUsername() != null &&
user.getUsername().length() >= 3;
}
}))).thenReturn(true);
// 测试匹配
assertTrue(mockDatabaseService.save(new User("validuser", "valid@example.com")));
// 【功能】使用 Lambda 表达式简化 Matcher
when(mockDatabaseService.update(argThat(user ->
user.getAge() >= 18 && user.getAge() <= 100
))).thenReturn(true);
User adultUser = new User("adult", "adult@example.com");
adultUser.setAge(25);
assertTrue(mockDatabaseService.update(adultUser));
}
}
3.1 Answer:用回调定义返回与副作用
-
场景:返回值取决于入参/状态 、需要副作用(计数、记录、抛异常)、或替代复杂依赖。
-
写法:
javawhen(mock.query(anyString())).thenAnswer(inv -> { String sql = inv.getArgument(0); if (sql.contains("admin")) return List.of(new User("admin","a@x.com")); if (sql.contains("user")) return List.of(new User("u1","1@x.com"), new User("u2","2@x.com")); return List.of(); });
InvocationOnMock inv
可拿到方法名、参数、调用次序等,便于分支返回。- 可做副作用 :记录参数、写测试日志、累加器、条件性
throw
。 - void 方法 用法:优先
doAnswer(...).when(mock).voidMethod(...)
。
-
注意:
- 逻辑要确定且轻量,避免在 Answer 里写"业务实现"(否则测试变脆)。
- 需要断言参数细节时,配合 ArgumentCaptor 更清晰。
- 若只是固定返回值,优先
thenReturn
,避免过度使用 Answer。
3.2 Matcher:用断言函数匹配复杂入参
-
场景:入参是复杂对象/集合/范围;等值匹配不足以表达业务约束。
-
写法:
javawhen(mock.save(argThat(new ArgumentMatcher<User>() { @Override public boolean matches(User u) { return u != null && u.getUsername()!=null && u.getUsername().length()>=3 && u.getEmail()!=null && u.getEmail().contains("@"); } }))).thenReturn(true); // Lambda 简化 when(mock.update(argThat(u -> u.getAge()>=18 && u.getAge()<=100))).thenReturn(true);
-
先判空再取字段,避免 NPE。
-
需要集合/Map时:
javawhen(mock.saveAll(argThat(list -> list!=null && list.size()==3)));
-
-
注意:
- 只做布尔判断,别在 Matcher 里做副作用。
- 同一调用中 不要混用"原始值"和"匹配器"(否则
InvalidUseOfMatchersException
);统一用eq(...)
/any()
/argThat(...)
。 - 复杂多字段校验 → 首选 Captor 做结构化断言;Matcher 负责"是否通过"。
3.3 常见技巧
-
异常分支:
javawhen(mock.query(argThat(sql -> sql==null || sql.isBlank()))) .thenAnswer(inv -> { throw new IllegalArgumentException("empty sql"); });
-
状态驱动:
javaAtomicInteger n = new AtomicInteger(); when(mock.next()).thenAnswer(inv -> n.getAndIncrement() < 2); // 前两次 true,之后 false
-
验证阶段也可用 Matcher:
javaverify(mock).save(argThat(u -> u.getUsername().startsWith("user")));