如何用JUnit5+Mockito覆盖void方法的ExecutionException捕获块
问题原因
你的测试代码里虽然mock了LoadingCache并设置抛出ExecutionException,但这个mock对象并没有注入到测试的MyService(即你的sut)实例中。因为MyService的attemptsCache是在构造函数里直接初始化的,Mockito的@InjectMocks无法替换这个私有且无注入入口的字段,导致实际执行unlockFailed时用的是真实缓存实例,不会触发异常,自然走不到catch分支。
解决方案
要覆盖catch块,核心是让sut中的attemptsCache使用我们mock的对象,下面给出两种可行方案:
方案1:通过反射替换私有字段(无需修改原业务代码)
修改测试代码,在准备阶段用反射把mock的缓存注入到sut的私有字段中:
private final String USER = "fakeuser"; @InjectMocks private MyService sut; // 注意要和你的业务类类名对应 @DisplayName("unlockFailed 应正确处理 ExecutionException") @Test public void unlockFailed_should_handle_ExecutionException() throws Exception { // Arrange LoadingCache<String, Integer> mockCache = Mockito.mock(LoadingCache.class); // 反射修改私有字段 Field cacheField = MyService.class.getDeclaredField("attemptsCache"); cacheField.setAccessible(true); cacheField.set(sut, mockCache); // 设置mock缓存的get方法抛出异常 doThrow(new ExecutionException("Dummy ExecutionException", null)) .when(mockCache).get(USER); // Act sut.unlockFailed(USER); // Assert // 验证catch块执行后,会调用put存入1(attempts初始0,catch后重置为0,自增后为1) Mockito.verify(mockCache, times(1)).get(USER); Mockito.verify(mockCache, times(1)).put(USER, 1); }
方案2:修改业务类,提供缓存注入入口(测试友好写法)
如果允许修改原业务代码,可以新增带缓存参数的构造函数,方便测试时直接注入mock对象:
// 修改后的MyService private LoadingCache<String, Integer> attemptsCache; // 保留原构造函数,兼容现有逻辑 public MyService() { this(createDefaultCache()); } // 新增构造函数,用于测试注入mock public MyService(LoadingCache<String, Integer> attemptsCache) { super(); this.attemptsCache = attemptsCache; } // 抽离默认缓存创建逻辑 private static LoadingCache<String, Integer> createDefaultCache() { return CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) .build(new CacheLoader<String, Integer>() { @Override public Integer load(String key) throws Exception { return 0; } }); } // unlockFailed方法保持不变 public void unlockFailed(String key) { int attempts = 0; try { attempts = attemptsCache.get(key); } catch (ExecutionException e) { attempts = 0; } attempts++; attemptsCache.put(key, attempts); }
对应的简化测试代码:
private final String USER = "fakeuser"; @DisplayName("unlockFailed 应正确处理 ExecutionException") @Test public void unlockFailed_should_handle_ExecutionException() throws ExecutionException { // Arrange LoadingCache<String, Integer> mockCache = Mockito.mock(LoadingCache.class); MyService sut = new MyService(mockCache); // 直接传入mock缓存 doThrow(new ExecutionException("Dummy ExecutionException", null)) .when(mockCache).get(USER); // Act sut.unlockFailed(USER); // Assert Mockito.verify(mockCache).get(USER); Mockito.verify(mockCache).put(USER, 1); }
关键验证逻辑
两种方案的核心断言点一致:
- 确认
attemptsCache.get(key)被调用一次(触发异常的前提) - 确认
attemptsCache.put(key, 1)被调用一次(证明catch块执行后,逻辑正常走完)
这样就能确保catch分支被实际执行,覆盖对应的代码块。
内容的提问来源于stack exchange,提问作者pixel
相关产品推荐
相关产品推荐

