在 Jackson / Spring Boot 中测试自定义 Json Deserializer

     2023-02-27     138

关键词:

【中文标题】在 Jackson / Spring Boot 中测试自定义 Json Deserializer【英文标题】:Testing custom JsonDeserializer in Jackson / SpringBoot 【发布时间】:2020-05-20 12:02:56 【问题描述】:

我正在尝试将单元测试写入自定义反序列化程序,该反序列化程序使用带有@Autowired 参数的构造函数和我的用@JsonDeserialize 标记的实体进行实例化。它在我的集成测试中运行良好,其中 MockMvc 带来了 spring 服务器端。

但是,在调用 objectMapper.readValue(...) 的测试中,使用没有参数的默认构造函数的反序列化器的新实例被实例化。虽然

@Bean
public MyDeserializer deserializer(ExternalObject externalObject) 

实例化解串器的有线版本,真正的调用仍然传递给空的构造函数并且上下文没有被填充。

我尝试手动实例化反序列化器实例并将其注册到 ObjectMapper 中,但它仅在我从实体类中删除 @JsonDeserialize 时才有效(即使我在 @Configuration 类中执行相同操作,它也会破坏我的集成测试。) - 看起来与此相关:https://github.com/FasterXML/jackson-databind/issues/1300

我仍然可以直接调用 deserializer.deserialize(...) 来测试反序列化器的行为,但是这种方法在不是反序列化器单元测试的测试中对我不起作用...

UPD:下面的工作代码

import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.github.tomakehurst.wiremock.common.Json;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;

import java.io.IOException;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

@JsonTest
@RunWith(SpringRunner.class)
public class JacksonInjectExample 
    private static final String JSON = "\"field1\":\"value1\", \"field2\":123";

    public static class ExternalObject 
        @Override
        public String toString() 
            return "MyExternalObject";
        
    

    @JsonDeserialize(using = MyDeserializer.class)
    public static class MyEntity 
        public String field1;
        public String field2;
        public String name;

        public MyEntity(ExternalObject eo) 
            name = eo.toString();
        

        @Override
        public String toString() 
            return name;
        
    

    @Component
    public static class MyDeserializer extends JsonDeserializer<MyEntity> 

        @Autowired
        private ExternalObject external;

        public MyDeserializer() 
            SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
        

        public MyDeserializer(@JacksonInject final ExternalObject external) 
            this.external = external;
        

        @Override
        public MyEntity deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
            JsonProcessingException 
            return new MyEntity(external);
        
    

    @Configuration
    public static class TestConfiguration 
        @Bean
        public ExternalObject externalObject() 
            return new ExternalObject();
        

        @Bean
        public MyDeserializer deserializer(ExternalObject externalObject) 
            return new MyDeserializer(externalObject);
        
    

    @Test
    public void main() throws IOException 
        HandlerInstantiator hi = mock(HandlerInstantiator.class);
        MyDeserializer deserializer = new MyDeserializer();
        deserializer.external = new ExternalObject();
        doReturn(deserializer).when(hi).deserializerInstance(any(), any(), eq(MyDeserializer.class));
        final ObjectMapper mapper = Json.getObjectMapper();
        mapper.setHandlerInstantiator(hi);

        final MyEntity entity = mapper.readValue(JSON, MyEntity.class);
        Assert.assertEquals("MyExternalObject", entity.name);
    

【问题讨论】:

【参考方案1】:

单元测试不应依赖或调用其他主要类或框架。如果还存在涵盖应用程序功能的集成或验收测试以及您描述的特定依赖项集,则尤其如此。所以最好编写单元测试,以便它有一个单一的类作为它的主题,即直接调用 deserializer.deserialize(...)。

在这种情况下,单元测试应包括使用模拟或存根 ExternalObject 实例化 MyDeserializer,然后测试其 deserialize() 方法是否针对 JsonParser 和 DeserializationContext 参数的不同状态正确返回 MyEntity。 Mockito 非常适合设置模拟依赖项!

通过在单元测试中使用 ObjectMapper,Jackson 框架中的大量代码也在每次运行中被调用 - 所以测试不是验证 MyDeserializer 的合约,它是验证 MyDeserializer 组合的行为和杰克逊的一个特定版本。如果测试失败,将无法立即清楚所涉及的所有组件中的哪一个有故障。而且由于同时设置两个框架的环境更加困难,随着时间的推移,测试将变得脆弱,并且由于测试类中的设置问题而更频繁地失败。

Jackson 框架负责使用 @JacksonInject 编写 ObjectMapper.readValue 和构造函数的单元测试。对于“不是反序列化器单元测试的其他单元测试” - 最好为该测试模拟/存根 MyDeserializer (或其他依赖项)。这样,其他类的逻辑与 MyDeserializer 中的逻辑隔离 - 并且可以验证其他类的合同,而无需通过被测单元之外的代码行为来限定。

【讨论】:

感谢您的意见。我知道从 ObjectMapper 调用它使其不是完全单元测试的方法,并且对于测试反序列化器本身就足够了,但是,不幸的是,我正在测试某些服务的其他测试与反序列化器的结果紧密耦合并模拟它会到某个扩展意味着在测试中重写其完整的逻辑。我知道这不是一个好的设计,但它现在不在我的手中,为了开始重构,我需要以现有的方式支持现有的测试。【参考方案2】:

我不知道如何设置这个,特别是使用 Jackson 注入,但你可以使用 spring Json 测试来测试它。我认为这种方法更接近真实场景,也更简单。 Spring 将仅加载与序列化/反序列化 bean 相关的内容,因此您必须仅提供自定义 bean 或模拟来代替它们。

@JsonTest
public class JacksonInjectExample 

  private static final String JSON = "\"field1\":\"value1\", \"field2\":123";

  @Autowired
  private JacksonTester<MyEntity> jacksonTester;

  @Configuration
  public static class TestConfiguration 
      @Bean
      public ExternalObject externalObject() 
          return new ExternalObject();
      
  

  @Test
  public void test() throws IOException 
      MyEntity result = jacksonTester.parseObject(JSON);
      assertThat(result.getName()).isEqualTo("MyExternalObject");
  

如果您想使用模拟,请使用以下 sn-p:

  @MockBean
  private ExternalObject externalObject;

  @Test
  public void test() throws IOException 
      when(externalObject.toString()).thenReturn("Any string");
      MyEntity result = jacksonTester.parseObject(JSON);
      assertThat(result.getName()).isEqualTo("Any string");
  

【讨论】:

谢谢您,您的代码有效,但是对我来说,通过 mapper.readValue(JSON, MyEntity.class) 的评估来进行解析对我来说很重要,因为我有测试可以在后台进行。 .【参考方案3】:

非常有趣的问题,它让我想知道自动装配到杰克逊反序列化器中实际上是如何在 Spring 应用程序中工作的。使用的杰克逊设施似乎是HandlerInstantiator interface,也就是configured by spring to the SpringHandlerInstantiator implementation,它只是在应用程序上下文中查找类。

所以理论上你可以在单元测试中使用你自己的(模拟的)HandlerInstantiator 设置一个ObjectMapper,从deserializerInstance() 返回一个准备好的实例。其他方法返回null好像没问题,或者类参数不匹配时,会导致jackson自己创建实例。

但是,我认为这不是对反序列化逻辑进行单元测试的好方法,因为ObjectMapper 设置必然不同于实际应用程序执行期间使用的设置。使用JsonTest annotation as suggested in Anton's answer 会是一种更好的方法,因为您将获得与运行时相同的 json 配置。

【讨论】:

我提前给了你赏金 :) 不幸的是,它不起作用。我已经使用您的解决方案使用代码更新了问题。出于某种原因,ObjectMapper 仍然尝试在没有 args 构造函数的情况下实例化 MyDeserializer 的实例,并且由于没有连接 ExternalObject 的上下文,我收到警告和后续的 NPE。 我看了看,可能只是小错误,试试eq(MyDeserializer.class) 而不是eq(MyEntity.class)。 Jackson 已经知道它必须从注解中寻找MyDeserializer 的实例,HandlerInstantiator 只是提供了一个对应的实例,它不负责为给定的实体类找到正确的反序列化器。 谢谢,它现在可以在这个简化的示例中使用!现在我必须弄清楚为什么这在我的实际测试中不起作用......可能是因为 CollectionType 反序列化器首先被实例化,它绕过模拟实例化器在其中创建 MyDeserializer 实例...... 想通了,它是 ObjectMapper 的另一个实例。此方法完美运行,无需使用反射 api 和其他丑陋的 hack。谢谢!

如何在 Spring Boot 1.4 中自定义 Jackson

】如何在SpringBoot1.4中自定义Jackson【英文标题】:HowtocustomiseJacksoninSpringBoot1.4【发布时间】:2017-01-0820:48:48【问题描述】:我一直无法找到如何在springboot1.4中使用Jackson2ObjectMapperBuilderCustomizer.java来自定义Jackson的功能的示例。在bo... 查看详情

在 Jackson / Spring Boot 中测试自定义 Json Deserializer

】在Jackson/SpringBoot中测试自定义JsonDeserializer【英文标题】:TestingcustomJsonDeserializerinJackson/SpringBoot【发布时间】:2020-05-2012:02:56【问题描述】:我正在尝试将单元测试写入自定义反序列化程序,该反序列化程序使用带有@Autowired参... 查看详情

在 Spring Boot 应用程序中配置 Jackson mixin

】在SpringBoot应用程序中配置Jacksonmixin【英文标题】:ConfiguringJacksonmixininSpringBootapplication【发布时间】:2018-12-1400:39:13【问题描述】:我为我的班级创建了一个mixin。mixin本身工作正常,这不是大多数人在混合faterxml/codehaus注释时... 查看详情

Jackson 在我的 Spring Boot 应用程序中忽略了 spring.jackson.properties

】Jackson在我的SpringBoot应用程序中忽略了spring.jackson.properties【英文标题】:Jacksonisignoringspring.jackson.propertiesinmyspringbootapplication【发布时间】:2017-03-3115:46:51【问题描述】:Jackson忽略了spring.jackson.property-naming-strategy=SNAKE_CASE 查看详情

如何在 Spring Boot REST API 中启用对 JSON / Jackson @RequestBody 的严格验证?

】如何在SpringBootRESTAPI中启用对JSON/Jackson@RequestBody的严格验证?【英文标题】:HowdoIenablestrictvalidationofJSON/Jackson@RequestBodyinSpringBootRESTAPI?【发布时间】:2019-09-2620:49:04【问题描述】:如果在JSON请求中指定了额外的参数,我该如何... 查看详情

如何在spring boot应用程序中配置Jackson而不覆盖纯java中的springs默认设置

】如何在springboot应用程序中配置Jackson而不覆盖纯java中的springs默认设置【英文标题】:HowtoconfigureJacksoninspringbootapplicationwithoutoverridingspringsdefaultsettinginpurejava【发布时间】:2018-07-0905:47:25【问题描述】:在我的SpringBoot应用程序... 查看详情

Spring Boot的Jackson JSON映射器[关闭]

】SpringBoot的JacksonJSON映射器[关闭]【英文标题】:JacksonJSONmapperbySpringBoot[closed]【发布时间】:2018-10-0218:42:52【问题描述】:我的问题很简单,Jackson2ObjectMapperBuilder只在响应的序列化中起作用,在请求的序列化中不起作用?谢谢!... 查看详情

Spring boot + Jackson - 始终将日期转换为 UTC

】Springboot+Jackson-始终将日期转换为UTC【英文标题】:Springboot+Jackson-AlwaysconvertdatestoUTC【发布时间】:2018-02-2418:09:12【问题描述】:我将日期保存在我的数据库中没有时间戳,所以我想标准化SpringBootRest控制器接收日期的方式,以... 查看详情

Jackson 在 Spring Boot 中反序列化 GeoJson Point

】Jackson在SpringBoot中反序列化GeoJsonPoint【英文标题】:JacksondeserializeGeoJsonPointinSpringBoot【发布时间】:2018-01-2414:51:11【问题描述】:我有一个@Entity模型,它的属性类型为com.vividsolutions.jts.geom.Point。当我尝试在@RestController中渲染这... 查看详情

spring-boot 使用啥版本的 Jackson?

】spring-boot使用啥版本的Jackson?【英文标题】:WhatversionofJacksondoesspring-bootuse?spring-boot使用什么版本的Jackson?【发布时间】:2018-05-2123:11:27【问题描述】:我正在努力确保以安全的方式使用spring-boot和Jackson。Jackson的某些版本中... 查看详情

Spring Boot Jackson:如何从 java 数组中删除空值?

】SpringBootJackson:如何从java数组中删除空值?【英文标题】:SpringBootJackson:Howtoremovenullsfromjavaarray?【发布时间】:2018-02-1118:40:45【问题描述】:在我的SpringBoot应用程序中,当我编写了一个类并且我想在序列化时从该类的对象中... 查看详情

是否有任何选项可以在 Spring Boot 中使用 Jackson 为 java.time.* 包注册一次 Serializer/Deserializer?

】是否有任何选项可以在SpringBoot中使用Jackson为java.time.*包注册一次Serializer/Deserializer?【英文标题】:IsthereanyoptiontoregisterSerializer/Deserializeronlyonceforjava.time.*packagesusingJacksoninSpringBoot?【发布时间】:2021-02-1801:05:56【问题描述】:... 查看详情

Jackson 在 Spring Boot Rest 应用程序中将日期更改为一天。

】Jackson在SpringBootRest应用程序中将日期更改为一天。【英文标题】:Jacksonischangingdatetoonedayoldinspringbootrestapplication.【发布时间】:2017-08-1309:13:54【问题描述】:我用springboot1.5.2创建了一个简单的应用程序。我正在传递日期,并且... 查看详情

Spring Boot 1.4 自定义内部 Jackson 反序列化

】SpringBoot1.4自定义内部Jackson反序列化【英文标题】:SpringBoot1.4CustomizeInternalJacksonDeserialization【发布时间】:2017-08-1000:08:10【问题描述】:我在application.properties中使用spring.jackson.deserialization.FAIL_ON_UNKNOWN_PROPERTIES=true使反序列化在... 查看详情

spring boot中各种数据不匹配如何处理jackson反序列化错误

】springboot中各种数据不匹配如何处理jackson反序列化错误【英文标题】:Howtohandlejacksondeserializationerrorforallkindsofdatamismatchinspringboot【发布时间】:2019-02-1105:51:18【问题描述】:我知道这里有一些类似的问题,关于如何解析ENUM,如... 查看详情

Spring boot - Jackson 日期序列化和反序列化

】Springboot-Jackson日期序列化和反序列化【英文标题】:Springboot-Jacksondateserializationanddeserialization【发布时间】:2019-04-0312:29:52【问题描述】:在SpringBoot中,序列化和反序列化由Jackson执行。默认情况下,Jackson通过将日期转换为GMT... 查看详情

Jackson Object Mapper 在提供扩展配置时不工作,但在 Spring Boot 中提供类级别/字段级别注释时工作

】JacksonObjectMapper在提供扩展配置时不工作,但在SpringBoot中提供类级别/字段级别注释时工作【英文标题】:JacksonObjectMappernotworkingwhenextendedconfigurationprovidedbutworkingwhenprovidingclasslevel/fieldlevelannotationsinSpringBoot【发布时间】:2019-02-2... 查看详情

Spring Boot:Jackson 不会从“application.properties”中获取配置

】SpringBoot:Jackson不会从“application.properties”中获取配置【英文标题】:SpringBoot:Jacksonwon\'tpickupconfigurationfrom"application.properties"【发布时间】:2018-02-2505:03:42【问题描述】:我有一个基于Maven的多模块SpringBoot应用程序。在... 查看详情