测试CompletableFuture get() catch块失败,如何正确编写测试用例?
如何覆盖私有方法中CompletableFuture的catch代码块?
问题描述
我需要为代码中私有方法的catch代码块编写测试用例:
调用的public方法如下:
public void addValidationResultToValidations(Instrument instrument, AssetDetail assetDetail) { List<ValidationResult> validationResults = getValidationResults(instrument, assetDetail); // .... some other logic }
其中getValidationResults是返回ValidationResult列表的私有方法,实现如下:
private List<ValidationResult> getValidationResults(Instrument instrument, AssetDetail assetDetail) { List<CompletableFuture<ValidationResult>> completableFutureList = new ArrayList<>(); // 此处实际是调用服务生成的Future列表 CompletableFuture<List<ValidationResult>> listCompletableFuture = CompletableFuture.allOf(completableFutureList.toArray(new CompletableFuture[completableFutureList.size()])) .thenApply(v -> completableFutureList.stream() .filter(Objects::nonNull) .map(CompletableFuture::join) .collect(Collectors.toList())); try { return listCompletableFuture.get(); } catch (InterruptedException | ExecutionException e) { System.out.println("exception {}"+ e); } return new ArrayList<>(); }
我编写的测试用例尝试触发异常以覆盖catch块,但报错:
org.opens4j.AssertionError: Expected java.lang.Exception to be thrown, but nothing was thrown
测试用例代码:
@Test public void whenGetIsCalledOnCompletableFutureThenThrowException(){ Instrument instrument = AssetUtils.getInstrument(); AssetDetail assetDetail = AssetUtils.getAssetDetail(); CompletableFuture<List<ValidationResult>> badFuture = new CompletableFuture<>(); badFuture.completeExceptionally(new Exception()); assertThrown(Exception.class, () -> { service.addValidationResultToValidations(instrument, assetDetail); }) }
错误原因
- 测试用例中的
badFuture未被业务代码使用:你创建的异常Future完全独立于业务逻辑,业务代码里的completableFutureList要么是空列表,要么是由实际服务调用生成的正常Future,所以listCompletableFuture.get()不会抛出异常。 - 业务方法不会向外抛出异常:即使
get()抛出异常,私有方法的catch块已经捕获了异常,只会打印日志并返回空列表,上层的addValidationResultToValidations方法不会把异常抛出去,所以你断言“会抛出Exception”必然失败。
解决方案
要覆盖catch块,需要让listCompletableFuture.get()抛出InterruptedException或ExecutionException,同时调整测试断言逻辑(因为上层方法不会抛异常)。推荐通过mock生成CompletableFuture的依赖服务来实现:
步骤1:Mock依赖服务,返回异常的CompletableFuture
假设completableFutureList是由某个服务(比如ValidationService)的方法生成的,用Mockito mock该服务,让它返回一个会抛出异常的CompletableFuture:
@Mock private ValidationService validationService; @InjectMocks private YourService service; // 你的业务服务类 @Test public void whenValidationFutureFails_thenCatchBlockIsTriggered() { Instrument instrument = AssetUtils.getInstrument(); AssetDetail assetDetail = AssetUtils.getAssetDetail(); // 让mock服务返回一个异常的CompletableFuture CompletableFuture<ValidationResult> failedFuture = new CompletableFuture<>(); failedFuture.completeExceptionally(new RuntimeException("Validation failed")); when(validationService.getValidationFuture(any(Instrument.class), any(AssetDetail.class))) .thenReturn(failedFuture); // 调用public方法 service.addValidationResultToValidations(instrument, assetDetail); // 验证catch块的逻辑:比如验证日志是否打印(可借助日志框架的测试工具,如Logback的Appender) // 或者验证后续逻辑是否基于空列表执行,比如验证validations是否为空 // 示例:如果addValidationResultToValidations会把结果存入某个集合,可断言该集合为空 // assertThat(service.getValidations()).isEmpty(); }
步骤2:验证catch块的执行
因为catch块里只做了日志打印,你可以用日志测试工具(如org.slf4j:slf4j-test)来验证日志是否输出了异常信息,确保catch块被执行。
备选方案:反射调用私有方法
如果无法mock依赖服务,也可以用反射直接调用私有方法,同时构造让get()抛出异常的场景:
@Test public void whenGetThrowsException_thenCatchBlockIsExecuted() throws Exception { Instrument instrument = AssetUtils.getInstrument(); AssetDetail assetDetail = AssetUtils.getAssetDetail(); // 反射获取私有方法 Method method = YourService.class.getDeclaredMethod("getValidationResults", Instrument.class, AssetDetail.class); method.setAccessible(true); // 构造包含异常Future的列表,让join()触发ExecutionException List<CompletableFuture<ValidationResult>> futureList = new ArrayList<>(); CompletableFuture<ValidationResult> failedFuture = new CompletableFuture<>(); failedFuture.completeExceptionally(new RuntimeException("Test exception")); futureList.add(failedFuture); // 如果你能通过反射替换业务方法中的completableFutureList,或者借助PowerMock模拟CompletableFuture的行为 // 执行私有方法并验证返回结果为空列表 List<ValidationResult> result = (List<ValidationResult>) method.invoke(service, instrument, assetDetail); assertThat(result).isEmpty(); }
注意事项
- 不要断言上层方法抛出异常,因为私有方法已经捕获了异常并返回空列表,上层方法不会向外抛异常。
- 优先通过mock依赖服务的方式构造异常场景,这更符合单元测试的原则,避免依赖反射或PowerMock这类侵入性工具。
内容的提问来源于stack exchange,提问作者Bille Ibinabo
相关产品推荐
相关产品推荐

