如何正确Mock Jackson ObjectMapper内部实例实现异常流程测试
内部实例化ObjectMapper的Mock实现方案
你遇到的测试失效问题核心原因是:业务方法在方法内部直接通过new创建了局部ObjectMapper实例,普通的Mockito实例mock无法穿透方法作用域拦截到这个内部实例的调用,因此无法触发预设的异常流程。
针对这个场景,有两种可落地的实现路径:
方案1:调整业务代码结构(长期可维护方案)
这是最推荐的解决方式——Jackson官方本身就建议ObjectMapper作为线程安全的重资源以单例形式复用,本来就不适合在方法内部每次调用都重新创建:
- 把ObjectMapper从方法内的局部变量,调整为类的成员变量,通过构造器注入的方式引入,替代方法内部的
new操作。改造后的代码示例:
@Component public class JsonUtil { // 类持有单例的ObjectMapper实例 private final ObjectMapper objectMapper; // 构造器注入,测试场景下可直接传入mock实例 public JsonUtil(ObjectMapper objectMapper) { this.objectMapper = objectMapper; // 保留原有的字段可见性配置 this.objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); } public String getProductCategoryJSON(NestedProductCategoryMap nestedProductCategoryMap) { try { // 直接使用类持有的实例,不再在方法内部new return objectMapper.writeValueAsString(nestedProductCategoryMap.getRoot()); } catch (Exception e) { LOGGER.error("error-code:{} error: JSON conversion failed ", JSON_CONVERSION.getErrorCode(), e); throw new ValidationException(ExceptionEnum.JSON_CONVERSION, e); } } }
- 这种改造后,你最初编写的Mockito测试代码可以直接生效:测试时手动构造JsonUtil实例,传入提前配置好异常抛出逻辑的mock ObjectMapper,就能精准覆盖异常分支。
- 附加收益:消除每次方法调用重复创建ObjectMapper的性能损耗,代码可测试性、可维护性都会明显提升。
方案2:临时测试场景下的构造方法拦截
如果短期内无法调整业务代码结构,可以使用Mockito 3.5+版本提供的mockConstruction能力,直接拦截ObjectMapper的构造方法,把所有内部new出来的ObjectMapper实例替换为mock对象:
- 示例测试代码如下(严格使用try-with-resources管理拦截作用域,避免污染其他测试):
@Test public void test_getProductCategoryJSON_Trigger_Conversion_Exception() { NestedProductCategoryMap testParam = new NestedProductCategoryMap(); testParam.makeCat(1,"("); testParam.makeCat(2,"s2"); // 开启ObjectMapper构造方法拦截,作用域仅覆盖try块内的代码 try (MockedConstruction<ObjectMapper> mockedConstruction = Mockito.mockConstruction(ObjectMapper.class, (mockInstance, context) -> { // 给拦截到的mock实例配置预期行为:调用序列化方法时抛出测试异常 Mockito.when(mockInstance.writeValueAsString(any())).thenThrow(new JsonProcessingException("mock test exception") {}); })) { // 调用业务方法时,内部new的ObjectMapper已经被替换为上面配置的mock ValidationException exception = Assertions.assertThrows(ValidationException.class, () -> jsonUtil.getProductCategoryJSON(testParam) ); // 校验抛出的异常类型是否和业务定义一致 Assertions.assertEquals(ExceptionEnum.JSON_CONVERSION, exception.getExceptionEnum()); // 校验序列化方法确实被调用过 Mockito.verify(mockedConstruction.constructed().get(0), Mockito.times(1)) .writeValueAsString(any()); } }
- 注意事项:
- 这种方式属于测试层面的补丁实现,会强耦合业务的内部实现细节,一旦后续业务调整不再在方法内部new ObjectMapper,对应的测试逻辑就会失效,不建议作为长期方案使用。
- 你最初编写的测试代码存在匹配错误:业务方法传给
writeValueAsString的参数是nestedProductCategoryMap.getRoot()的返回值,不是NestedProductCategoryMap对象本身,就算mock能被拦截,之前配置的any(NestedProductCategoryMap.class)匹配规则也无法命中,不会触发抛异常的逻辑。
内容的提问来源于stack exchange,提问作者Windula Kularatne
相关产品推荐
相关产品推荐

