Mockito测试中ArgumentCaptor偶发失败问题求助
问题:Mockito测试偶现调用次数不匹配
环境信息
- Java 17
- Spring Boot 2.7.5(通过
spring-boot-starter-test引入JUnit Jupiter和Mockito)
待测试方法
public void verifyUserIsClubAdminOfAnyClub(@NonNull String username, @NonNull Set<Long> clubIds) { if (clubIds.stream() .map(clubAdminService::getAdminsOfClub) .flatMap(Set::stream) .noneMatch(username::equals)) { throw new AccessDeniedException("..."); } }
其中getAdminsOfClub方法返回Set<String>类型。
测试用例
@Test void testVerifyUserIsClubAdminOfAnyClubSuccess() { String username = "username"; Set<Long> clubIds = Set.of(1L, 2L); when(clubAdminService.getAdminsOfClub(1L)).thenReturn(Set.of("some user", "another user")); when(clubAdminService.getAdminsOfClub(2L)).thenReturn(Set.of("username", "some user")); ArgumentCaptor<Long> clubIdArgumentCaptor = ArgumentCaptor.forClass(Long.class); authorizationService.verifyUserIsClubAdminOfAnyClub(username, clubIds); verify(clubAdminService, times(2)).getAdminsOfClub(clubIdArgumentCaptor.capture()); assertThat(clubIdArgumentCaptor.getAllValues()).containsExactlyInAnyOrder(1L, 2L); }
问题现象
- 该测试在IntelliJ IDEA中全部通过,但在Eclipse中仅约80%的运行次数能通过(Eclipse测试执行速度约为IDEA的一半)
- 失败时的堆栈信息:
org.mockito.exceptions.verification.TooFewActualInvocations: clubAdminService.getAdminsOfClub( <Capturing argument> ); Wanted 2 times: -> at de.tonypsilon.bmm.backend.security.rnr.service.AuthorizationServiceTest.testVerifyUserIsClubAdminOfAnyClubSuccess(AuthorizationServiceTest.java:39) But was 1 time: -> at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) at de.tonypsilon.bmm.backend.security.rnr.service.AuthorizationServiceTest.testVerifyUserIsClubAdminOfAnyClubSuccess(AuthorizationServiceTest.java:39) ...(省略后续堆栈信息)
原因分析
问题核心是Stream的短路特性:noneMatch属于短路操作,一旦找到匹配元素(即某俱乐部的管理员集合包含目标用户名),就会立即终止流的遍历,不会处理后续的clubId。
而Set的遍历顺序不固定,测试执行时会出现两种情况:
- 如果先遍历
2L,调用getAdminsOfClub(2L)后发现包含username,流直接终止,仅调用1次方法 - 如果先遍历
1L,调用后不匹配,才会继续遍历2L,此时调用2次方法
测试中硬编码要求调用times(2),导致遍历顺序不同时出现偶现失败。
解决方案
方案1:修改测试,兼容短路场景
不再强制固定调用次数,转而验证核心业务逻辑:方法执行不抛出异常,且至少有一次调用的clubId对应管理员集合包含目标用户名:
@Test void testVerifyUserIsClubAdminOfAnyClubSuccess() { String username = "username"; Set<Long> clubIds = Set.of(1L, 2L); when(clubAdminService.getAdminsOfClub(1L)).thenReturn(Set.of("some user", "another user")); when(clubAdminService.getAdminsOfClub(2L)).thenReturn(Set.of("username", "some user")); ArgumentCaptor<Long> clubIdArgumentCaptor = ArgumentCaptor.forClass(Long.class); // 验证方法执行不抛出异常 assertDoesNotThrow(() -> authorizationService.verifyUserIsClubAdminOfAnyClub(username, clubIds)); // 捕获所有调用的clubId,允许至少一次调用 verify(clubAdminService, atLeastOnce()).getAdminsOfClub(clubIdArgumentCaptor.capture()); // 验证捕获的clubId中,至少有一个对应的管理员集合包含目标用户名 assertThat(clubIdArgumentCaptor.getAllValues()) .anyMatch(clubId -> { if (clubId.equals(1L)) { return Set.of("some user", "another user").contains(username); } else if (clubId.equals(2L)) { return Set.of("username", "some user").contains(username); } return false; }); }
方案2:拆分测试场景,覆盖所有分支
分别测试两种短路情况,确保所有代码分支都被覆盖:
// 场景1:第一个俱乐部匹配,仅调用一次方法 @Test void testVerifyUserIsClubAdminOfAnyClubFirstClubMatches() { String username = "username"; Set<Long> clubIds = Set.of(2L, 1L); // 明确指定顺序,让匹配的clubId先遍历 when(clubAdminService.getAdminsOfClub(2L)).thenReturn(Set.of("username", "some user")); when(clubAdminService.getAdminsOfClub(1L)).thenReturn(Set.of("some user", "another user")); ArgumentCaptor<Long> clubIdArgumentCaptor = ArgumentCaptor.forClass(Long.class); authorizationService.verifyUserIsClubAdminOfAnyClub(username, clubIds); verify(clubAdminService, times(1)).getAdminsOfClub(clubIdArgumentCaptor.capture()); assertThat(clubIdArgumentCaptor.getAllValues()).containsExactly(2L); } // 场景2:第二个俱乐部匹配,调用两次方法 @Test void testVerifyUserIsClubAdminOfAnyClubSecondClubMatches() { String username = "username"; Set<Long> clubIds = Set.of(1L, 2L); when(clubAdminService.getAdminsOfClub(1L)).thenReturn(Set.of("some user", "another user")); when(clubAdminService.getAdminsOfClub(2L)).thenReturn(Set.of("username", "some user")); ArgumentCaptor<Long> clubIdArgumentCaptor = ArgumentCaptor.forClass(Long.class); authorizationService.verifyUserIsClubAdminOfAnyClub(username, clubIds); verify(clubAdminService, times(2)).getAdminsOfClub(clubIdArgumentCaptor.capture()); assertThat(clubIdArgumentCaptor.getAllValues()).containsExactlyInAnyOrder(1L, 2L); }
方案3:修改待测试方法(不推荐)
如果业务允许,可以放弃短路优化强制遍历所有clubIds,但这会降低性能,仅在特殊场景下考虑:
public void verifyUserIsClubAdminOfAnyClub(@NonNull String username, @NonNull Set<Long> clubIds) { // 用collect强制遍历所有元素,放弃短路 boolean isAdmin = clubIds.stream() .map(clubAdminService::getAdminsOfClub) .flatMap(Set::stream) .collect(Collectors.toSet()) .contains(username); if (!isAdmin) { throw new AccessDeniedException("..."); } }
内容的提问来源于stack exchange,提问作者Tonypsilon
相关产品推荐
相关产品推荐

