【问题标题】:How to unit-test Jackson JsonSerializer and JsonDeserializer如何对 Jackson JsonSerializer 和 JsonDeserializer 进行单元测试
【发布时间】:2014-02-14 18:42:56
【问题描述】:

我已经为我的应用编写了自定义 JsonSerializer 和 JsonDeserializer。 现在我想为他们写一些单元测试。

一个干净的测试用例应该是什么样子的?

那里有一些干净的例子吗?

(干净意味着不依赖于其他框架或库)

【问题讨论】:

    标签: unit-testing jackson


    【解决方案1】:

    JsonSerializer

    该示例正在序列化 LocalDateTime,但这可以替换为所需的类型。

    @Test
    public void serialises_LocalDateTime() throws JsonProcessingException, IOException {
        Writer jsonWriter = new StringWriter();
        JsonGenerator jsonGenerator = new JsonFactory().createGenerator(jsonWriter);
        SerializerProvider serializerProvider = new ObjectMapper().getSerializerProvider();
        new LocalDateTimeJsonSerializer().serialize(LocalDateTime.of(2000, Month.JANUARY, 1, 0, 0), jsonGenerator, serializerProvider);
        jsonGenerator.flush();
        assertThat(jsonWriter.toString(), is(equalTo("\"2000-01-01T00:00:00\"")));
    }
    

    JsonDeserializer

    该示例正在反序列化 Number,但这可以替换为所需的类型。

        private ObjectMapper mapper;
        private CustomerNumberDeserialiser deserializer;
    
        @Before
        public void setup() {
            mapper = new ObjectMapper();
            deserializer = new CustomerNumberDeserialiser();
        }
    
        @Test
        public void floating_point_string_deserialises_to_Double_value() {
            String json = String.format("{\"value\":%s}", "\"1.1\"");
            Number deserialisedNumber = deserialiseNumber(json);
            assertThat(deserialisedNumber, instanceOf(Double.class));
            assertThat(deserialisedNumber, is(equalTo(1.1d)));
        }
    
        @SneakyThrows({JsonParseException.class, IOException.class})
        private Number deserialiseNumber(String json) {
            InputStream stream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
            JsonParser parser = mapper.getFactory().createParser(stream);
            DeserializationContext ctxt = mapper.getDeserializationContext();
            parser.nextToken();
            parser.nextToken();
            parser.nextToken();
            return deserializer.deserialize(parser, ctxt);
        }
    

    更新

    升级到 jackson 2.9.3 时,在将字符串反序列化为数字时,我在 DeserializationContextisEnabled(MapperFeature feature) 中收到了一个 NullPointerException,因为 new ObjectMapper()_config 初始化为 null

    为了解决这个问题,我使用了this SO 答案来监视finalDeserializationContext

    DeserializationContext ctxt = spy(mapper.getDeserializationContext());
    doReturn(true).when(ctxt).isEnabled(any(MapperFeature.class));
    

    我觉得一定有更好的方法,所以如果你有的话,请评论。

    【讨论】:

    • 对于那些像我一样想知道parser.nextToken 是什么的人在这里重复了三遍——这意味着我们正在尝试将解析器从 json {value:1.1} 移动到值 1.1parser.nextToken 需要 3 次,因为顺序是 START_OBJECT、FIELD_NAME、VALUE_STRING,我们希望在示例中达到 VALUE_STRING
    • @nilesh :有没有办法避免 parser.nextToken 重复 3 次?我有类似的要求,但不想使用 parser.nextToken 3 次
    • 我查看了JsonParser APIJsonParser source,我不相信有。
    • 我认为也没有办法避免解析令牌修复
    • 我还注意到不再需要InputStream。可以简单地使用mapper.getFactory().createParser(json)
    【解决方案2】:

    其他答案已经很好地涵盖了序列化。我还有另一个反序列化建议。

    如果您的域类具有@JsonDeserialize 注释,那么您的反序列化器将被默认的 ObjectMapper 拾取,而无需显式注册。

    @JsonDeserialize(using=ExampleDeserializer.class)
    public class Example {
        private String name;
        private BananaStore bananaStore;
    
        public Example(String name, BananaStore bananaStore) {
            this.name = name;
            this.bananaStore = bananaStore;
        }
    
        public String getName() {
            return name;
        }
    
        public BananaStore getBananaStore() {
            return bananaStore;
        }
    }
    

    你的单元测试可以变得更短:

    public class ExampleDeserializerTest {
    
        @Test
        public void deserialize() throws IOException {
            String json = "{\"name\":\"joe\",\"bananas\":16}";
    
            Example example = new ObjectMapper().readValue(json, Example.class);
    
            assertEquals("joe", example.getName());
            assertEquals(16, example.getBananaStore().getBananaCount());
        }
    }
    

    【讨论】:

      【解决方案3】:

      可以像这样对反序列化器进行单元测试:

      public class CustomFooDeserializerTest {
          
          private Foo foo;
          
          @Before
          public void setUp() {
              ObjectMapper objectMapper = new ObjectMapper();
              SimpleModule module = new SimpleModule();
              module.addDeserializer(Foo.class, new CustomFooDeserializer());
              objectMapper.registerModule(module);
              foo = objectMapper.readValue(new File("path/to/file"), Foo.class);
          }
          
          @Test
          public void shouldSetSomeProperty() {
              assertThat(foo.getSomeProperty(), is("valueOfSomeProperty"));
          }
          
      }   
      

      【讨论】:

      • 分号前的行尾缺少括号 ):assertThat(foo.getSomeProperty(), is("valueOfSomeProperty"); 我无法编辑,因为它只有 1 个字符 :-) .
      【解决方案4】:

      我没有找到任何示例,但您可以尝试创建一个生成器,例如:

      StringWriter stringJson = new StringWriter();
      JsonGenerator generator = new JsonFactory().createGenerator(stringJson);
      

      您可以使用

      获得SerializerProvider 实例
      new ObjectMapper().getSerializerProvider();
      

      【讨论】:

      • ...我们可以执行'stringJson.close(); stringJson.toString();'获取生成为字符串的 json :)
      • FWIW StringWriter.close() 不会按照 docs 做任何事情,但关闭它还是有意义的,因为它实现了 CloseableAutocloseable
      【解决方案5】:

      我找到了一种对反序列化器进行单元测试的方法,但很难弄清楚。看看我的回购https://bitbucket.org/arbeitsgruppedenktmit/de.denktmit.rest.hal

      单元测试类在这里: de.denktmit.rest.hal/src/test/java/de/denktmit/rest/hal/jackson/RelationDeserializerTest.java

      可以在此处找到用于在上下文中测试反序列化器的集成测试: de.denktmit.rest.hal/src/test/java/de/denktmit/rest/hal/OrderResourceIntegrationTest.java

      因评论而编辑 设置映射器以方便单元测试的基类

      public abstract class AbstractJackson2MarshallingTest {
      
          protected ObjectMapper mapper;
      
          @Before
          public void setUp() {
              mapper = new ObjectMapper();
              mapper.enable(SerializationFeature.INDENT_OUTPUT);
          }
      
          protected String write(Object object) throws Exception {
              Writer writer = new StringWriter();
              mapper.writeValue(writer, object);
              return writer.toString();
          }
      
          protected <T> T read(String source, Class<T> targetType) throws Exception {
              return mapper.readValue(source, targetType);
          }
      
          protected String getPackagePath() {
              return "/" + this.getClass().getPackage().getName().replace('.', '/');
          }
      }
      

      包含单元测试的测试类

      public class RelationDeserializerTest extends AbstractJackson2MarshallingIntegrationTest {
      
          @Rule public ResourceFile resourceFile = new ResourceFile(getPackagePath() + "/single_valued_relation.json");
          @Rule public ResourceFile resourceFile2 = new ResourceFile(getPackagePath() + "/multi_valued_relation.json");
      
          private RelationDeserializer deserializer = new RelationDeserializer();
      
          @Test
          public void testDeserializeSingleValuedRelation() throws IOException {
              resourceFile = new ResourceFile(getPackagePath() + "/single_valued_relation.json");
              JsonParser parser = mapper.getFactory().createParser(resourceFile.getContent());
              DeserializationContext ctxt = mapper.getDeserializationContext();
              SingleValuedRelation rel = (SingleValuedRelation) deserializer.deserialize(parser, ctxt);
              assertEquals(rel.getName(), "invoiceAddress");
              assertEquals("invoiceAddressURL", rel.getLink().getHref());
              assertEquals("linkName", rel.getLink().getName());
              assertEquals("de", rel.getLink().getHreflang());
              assertNull(parser.nextToken());
          }
      
          @Test
          public void testDeserializeMultiValuedRelation() throws IOException {
              resourceFile = new ResourceFile(getPackagePath() + "/multi_valued_relation.json");
              JsonParser parser = mapper.getFactory().createParser(resourceFile.getContent());
              DeserializationContext ctxt = mapper.getDeserializationContext();
              MultiValuedRelation rel = (MultiValuedRelation) deserializer.deserialize(parser, ctxt);
              assertEquals(rel.getName(), "images");
              Iterator<Link> linkIterator = rel.getLinks().iterator();
              Link link = linkIterator.next();
              assertEquals("imageUrl1", link.getHref());
              link = linkIterator.next();
              assertEquals("imageUrl2", link.getHref());
              assertNull(parser.nextToken());
          }
      
      }
      

      被测类

      public class RelationDeserializer extends StdDeserializer<Relation> {
      
          public RelationDeserializer() {
              super(Relation.class);
          }
      
          @Override
          public Relation deserialize(JsonParser p, DeserializationContext ctxt)
                  throws IOException, JsonProcessingException {
              if (p.getCurrentToken() == null && p.nextToken() == null) {
                  String msg = getClass().getCanonicalName()
                          + ": Can not deserialize without token";
                  throw new IOException(msg);
              }
              if (p.getCurrentToken() != JsonToken.START_OBJECT
                      && p.getCurrentToken() != JsonToken.START_ARRAY) {
                  String msg = getClass().getCanonicalName()
                          + ": Expected data to start with an Relation object or an array of Relation objects";
                  throw new IOException(msg);
              }
              if (p.nextToken() != JsonToken.FIELD_NAME) {
                  String msg = getClass().getCanonicalName()
                          + ": Expected relation to be started by a field name";
                  throw new IOException(msg);
              }
              String relationName = p.getText();
              JsonToken tok = p.nextToken();
              Relation rel;
              switch (tok) {
              case START_ARRAY:
                  rel = createMultiValuedRelation(relationName, p);
                  break;
              case START_OBJECT:
                  rel = createSingleValuedRelation(relationName, p);
                  break;
              default:
                  String msg = getClass().getCanonicalName() + "Expected relation content is a single link or array of links";
                  throw new IOException(msg);
              }
              p.nextToken();
              return rel;
          }
      
          private Relation createMultiValuedRelation(String relationName, JsonParser p)
                  throws JsonParseException, IOException {
              List<Link> links = new ArrayList<Link>();
              if (p.nextToken() == JsonToken.START_OBJECT) {
                  Iterator<DefaultLink> linkIterator = p.readValuesAs(DefaultLink.class);
                  while (linkIterator.hasNext()) {
                      links.add(linkIterator.next());
                  }
              } 
              if (p.getCurrentToken() != JsonToken.END_ARRAY) {
                  String msg = getClass().getCanonicalName() + "Expected relation content is a single link or (possibly empty) array of links";
                  throw new IOException(msg);
              }
              return RelationFactory.createRelation(relationName, links);
          }
      
          private Relation createSingleValuedRelation(String relationName,
                  JsonParser p) throws JsonParseException, IOException {
              return RelationFactory.createRelation(relationName, p.readValueAs(DefaultLink.class));
          }
      
      
      }
      

      希望对您有所帮助并致以最诚挚的问候

      【讨论】:

      • @Rule 资源文件只是一个助手,可以轻松地从 test/resources 中的文件中读取 json
      【解决方案6】:

      我开始使用@RobertBain 的答案中的解决方案,但它对我不起作用,因为 JSON 解析器以某种方式不断失去价值。我最终包含了 Mockito 并在我的反序列化器使用的 JsonParser 上存根该方法,并使用它编写了我的测试。请看下面的一个小例子:

      public class ZoneDateTimeDeserializerTest {
          private ZoneDateTimeDeserializer deserializer;
      
          @Mock
          private JsonParser parser;
      
          @Rule
          public MockitoRule mockitoRule = MockitoJUnit.rule();
      
          @Before
          public void setUp() {
              deserializer = new ZoneDateTimeDeserializer();
              TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
          }
      
          @Test
          public void givenAZonedDateTimeInIsoFormat_whenTryingToDeserializeIt_itShouldBeSuccessful() {
              final String date = "1996-01-29T22:40:54+02:00";
      
              ZonedDateTime dateTime = deserialiseZoneDateTime(date);
      
              Assert.assertNotNull(dateTime);
              assertThat(dateTime, instanceOf(ZonedDateTime.class));
              Assert.assertEquals(1996, dateTime.getYear());
              Assert.assertEquals(Month.JANUARY, dateTime.getMonth());
              Assert.assertEquals(29, dateTime.getDayOfMonth());
              Assert.assertEquals(20, dateTime.getHour());
              Assert.assertEquals(40, dateTime.getMinute());
              Assert.assertEquals(54, dateTime.getSecond());
          }
      
          @SneakyThrows({JsonParseException.class, IOException.class})
          private ZonedDateTime deserialiseZoneDateTime(String json) {
              // Mock the parser so that it returns the value when getValueAsString is called on it
              when(parser.getValueAsString()).thenReturn(json);
              return deserializer.deserialize(parser, null);
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2016-02-26
        • 2012-01-08
        • 2019-11-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-09-30
        相关资源
        最近更新 更多