JUnit单元测试通过但无代码覆盖率问题排查(Mockito)
问题背景
为局部更新(Patch调用)的服务方法编写单元测试时,出现测试执行通过但代码覆盖率统计异常的问题:测试可以覆盖被测方法内部调用的子方法,但被测方法本身的代码无法被覆盖率工具统计。
原有测试实现
@MockitoSettings(strictness = Strictness.LENIENT) @Test public void updateProductPartialTest() throws Exception { Product prod = saveprod(); ProductDto dto = saveDto(); PartialProductDto updto = patchDto(); Product upprod = upprod(); ProductDto updtoo = updateDto(); String id = "5e7e2fcf-c0b8-4c9b-860b-55d71a3c580f"; given(mapper.ProductDtotoProduct(dto)).willReturn(prod); given(mapper.ProductToProductDto(prod)).willReturn(dto); given(productrepository.findByProductId(dto.getProductId())).willReturn(Optional.empty()); given(productrepository.save(prod)).willReturn(prod); service.addProduct(dto); given(productrepository.findByProductId(dto.getProductId())).willReturn(Optional.of(prod)); given(service.mappingForPatrialUpdate(updto, prod)).willReturn(upprod); Mockito.doReturn(updtoo).when(service).updateProductPartial(updto, id); given(mapper.ProductToProductDto(prod)).willReturn(dto); given(mapper.ProductDtotoProduct(dto)).willReturn(prod); given(service.addProduct(dto)).willReturn(dto); service.addProduct(dto); service.updateProductPartial(updto, id); given(productrepository.findByProductId(id)).willReturn(Optional.of(upprod)); assertThat(productrepository.findByProductId(id).get().equals(upprod)); }
测试Mock配置
@Mock ProductRepository productrepository; @Mock SimpleProductdtoProductMapper mapper; @Spy @InjectMocks ProductServiceImpl service; @InjectMocks ProductGetServiceImpl serviceget;
问题根因
判断完全准确:问题由Mockito.doReturn(updtoo).when(service).updateProductPartial(updto, id);这行代码导致。
当前service实例使用@Spy注解修饰为间谍对象,这行代码直接对被测方法本身做了桩设置,后续调用service.updateProductPartial(updto, id)时,Mockito会直接返回预设的桩值updtoo,完全不会执行方法内部的真实业务逻辑,覆盖率工具自然无法采集到该方法的执行数据。
除此之外原有测试还存在两个影响覆盖率和测试有效性的问题:
- 存在大量冗余的重复桩配置,甚至对非本次测试目标的
addProduct方法也做了不必要的桩化和调用,干扰测试逻辑 - 断言写法无效:
assertThat(productrepository.findByProductId(id).get().equals(upprod));仅传入了一个boolean值,没有做真正的结果校验,且调用的是Mock出来的repository方法,校验无实际意义
修复方案
- 核心修改:删除对被测方法
updateProductPartial的桩化代码,保证调用该方法时执行真实业务逻辑。 - 桩规则仅保留被测方法内部依赖的外部组件、需要隔离的内部子方法的配置,不需要桩化被测方法本身,也不需要保留和当前测试目标无关的
addProduct相关调用与桩配置。 - 修正断言逻辑:直接对被测方法的真实返回值做校验,必要时可通过
verify验证内部依赖的调用是否符合预期。
修正后的参考测试代码如下:
@MockitoSettings(strictness = Strictness.LENIENT) @Test public void updateProductPartialTest() throws Exception { // 初始化测试数据 Product existingProd = saveprod(); PartialProductDto patchDto = patchDto(); Product updatedProd = upprod(); ProductDto expectedResult = updateDto(); String targetId = "5e7e2fcf-c0b8-4c9b-860b-55d71a3c580f"; // 仅桩化被测方法的外部依赖、需要隔离的内部子方法 given(productrepository.findByProductId(targetId)).willReturn(Optional.of(existingProd)); given(service.mappingForPatrialUpdate(patchDto, existingProd)).willReturn(updatedProd); given(productrepository.save(updatedProd)).willReturn(updatedProd); given(mapper.ProductToProductDto(updatedProd)).willReturn(expectedResult); // 调用真实被测方法 ProductDto actualResult = service.updateProductPartial(patchDto, targetId); // 校验结果与交互逻辑 assertThat(actualResult).isEqualTo(expectedResult); verify(productrepository).findByProductId(targetId); verify(service).mappingForPatrialUpdate(patchDto, existingProd); verify(productrepository).save(updatedProd); }
注意:使用
@Spy修饰被测服务实例时,遵循「桩化所有外部依赖、不桩化被测方法本身」的原则即可。对于被测类内部不需要隔离的子方法(比如简单的参数校验、格式转换逻辑),不需要额外桩化,直接走真实逻辑反而能统计到更完整的覆盖率。
内容的提问来源于stack exchange,提问作者Ali Raza
相关产品推荐
相关产品推荐

