Testing an isolated custom JsonDeserializer in Java

时间:2017-04-06 16:54:18

标签: java json serialization gson

So for this little program I'm writing I'm looking to parse Twitter's tweet stream. Im using the Gson library which works nice. Gson couldn't parse Twitters created_at datetime field, so I had to write a custom JsonDserializer that needs to be registered with the parser through the GsonBuilderas follows:

new GsonBuilder().registerTypeAdatapter(DateTime.class, <myCustomDeserializerType>)

Now my deserializer works well, and I am able to parse Twitter's stream.

However, I'm trying to cover as much of my program with unit tests, so this custom deserializer should be included.

Since a good unit test is a nicely isolated test, I do not want to register it with a Gson object after which I would parse a json string. What I do want is to create an instance of my deserializer and just pass a generic string representing a datetime, so that I could test the deserializer without it being integrated with anything else.

The signature of the deserialize method of a JsonDeserializer is as follows:

deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext)

Let's say I want to parse the following data: 'Mon Mar 27 14:09:47 +0000 2017'. How would I have to transform my input data in order to correctly test my deserializer.

I'm not looking for code that actually parses this date, I already have that part covered. I'm asking how I can meet the deserialize method's signature so that I can simulate it's use in a Gson it is used in.

1 个答案:

答案 0 :(得分:6)

JsonSerializerJsonDeserializer紧密绑定到Gson JSON树模型和特定的Gson配置(de)序列化上下文,它提供了一组可以(反)序列化的类型。因此,完成JsonSerializerJsonDeserializer的单元测试相当困难。

请考虑src/test/resources/.../zoned-date-time.json中的以下JSON文档:

"Mon Mar 27 14:09:47 +0000 2017"

这是一个非常有效的JSON文档,除了简单的单个字符串外,它没有任何内容。上述格式的日期/时间格式化程序可以在Java 8中实现,如下所示:

final class CustomPatterns {

    private CustomPatterns() {
    }

    private static final Map<Long, String> dayOfWeek = ImmutableMap.<Long, String>builder()
            .put(1L, "Mon")
            .put(2L, "Tue")
            .put(3L, "Wed")
            .put(4L, "Thu")
            .put(5L, "Fri")
            .put(6L, "Sat")
            .put(7L, "Sun")
            .build();

    private static final Map<Long, String> monthOfYear = ImmutableMap.<Long, String>builder()
            .put(1L, "Jan")
            .put(2L, "Feb")
            .put(3L, "Mar")
            .put(4L, "Apr")
            .put(5L, "May")
            .put(6L, "Jun")
            .put(7L, "Jul")
            .put(8L, "Aug")
            .put(9L, "Sep")
            .put(10L, "Oct")
            .put(11L, "Nov")
            .put(12L, "Dec")
            .build();

    static final DateTimeFormatter customDateTimeFormatter = new DateTimeFormatterBuilder()
            .appendText(DAY_OF_WEEK, dayOfWeek)
            .appendLiteral(' ')
            .appendText(MONTH_OF_YEAR, monthOfYear)
            .appendLiteral(' ')
            .appendValue(DAY_OF_MONTH, 1, 2, NOT_NEGATIVE)
            .appendLiteral(' ')
            .appendValue(HOUR_OF_DAY, 2)
            .appendLiteral(':')
            .appendValue(MINUTE_OF_HOUR, 2)
            .appendLiteral(':')
            .appendValue(SECOND_OF_MINUTE, 2)
            .appendLiteral(' ')
            .appendOffset("+HHMM", "+0000")
            .appendLiteral(' ')
            .appendValue(YEAR)
            .toFormatter();

}

现在考虑ZonedDateTime的以下JSON反序列化器:

final class ZonedDateTimeJsonDeserializer
        implements JsonDeserializer<ZonedDateTime> {

    private static final JsonDeserializer<ZonedDateTime> zonedDateTimeJsonDeserializer = new ZonedDateTimeJsonDeserializer();

    private ZonedDateTimeJsonDeserializer() {
    }

    static JsonDeserializer<ZonedDateTime> getZonedDateTimeJsonDeserializer() {
        return zonedDateTimeJsonDeserializer;
    }

    @Override
    public ZonedDateTime deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        try {
            final String s = context.deserialize(jsonElement, String.class);
            return ZonedDateTime.parse(s, customDateTimeFormatter);
        } catch ( final DateTimeParseException ex ) {
            throw new JsonParseException(ex);
        }
    }

}

请注意,我通过意图通过上下文对字符串进行了解释,以强调更复杂的JsonDeserializer实例可能会严重依赖它。现在让我们进行一些JUnit测试来测试它:

public final class ZonedDateTimeJsonDeserializerTest {

    private static final TypeToken<ZonedDateTime> zonedDateTimeTypeToken = new TypeToken<ZonedDateTime>() {
    };

    private static final ZonedDateTime expectedZonedDateTime = ZonedDateTime.of(2017, 3, 27, 14, 9, 47, 0, UTC);

    @Test
    public void testDeserializeIndirectlyViaAutomaticTypeAdapterBinding()
            throws IOException {
        final JsonDeserializer<ZonedDateTime> unit = getZonedDateTimeJsonDeserializer();
        final Gson gson = new GsonBuilder()
                .registerTypeAdapter(ZonedDateTime.class, unit)
                .create();
        try ( final JsonReader jsonReader = getPackageResourceJsonReader(ZonedDateTimeJsonDeserializerTest.class, "zoned-date-time.json") ) {
            final ZonedDateTime actualZonedDateTime = gson.fromJson(jsonReader, ZonedDateTime.class);
            assertThat(actualZonedDateTime, is(expectedZonedDateTime));
        }
    }

    @Test
    public void testDeserializeIndirectlyViaManualTypeAdapterBinding()
            throws IOException {
        final JsonDeserializer<ZonedDateTime> unit = getZonedDateTimeJsonDeserializer();
        final Gson gson = new Gson();
        final TypeAdapterFactory typeAdapterFactory = newFactoryWithMatchRawType(zonedDateTimeTypeToken, unit);
        final TypeAdapter<ZonedDateTime> dateTypeAdapter = typeAdapterFactory.create(gson, zonedDateTimeTypeToken);
        try ( final JsonReader jsonReader = getPackageResourceJsonReader(ZonedDateTimeJsonDeserializerTest.class, "zoned-date-time.json") ) {
            final ZonedDateTime actualZonedDateTime = dateTypeAdapter.read(jsonReader);
            assertThat(actualZonedDateTime, is(expectedZonedDateTime));
        }
    }

    @Test
    public void testDeserializeDirectlyWithMockedContext()
            throws IOException {
        final JsonDeserializer<ZonedDateTime> unit = getZonedDateTimeJsonDeserializer();
        final JsonDeserializationContext mockContext = mock(JsonDeserializationContext.class);
        when(mockContext.deserialize(any(JsonElement.class), eq(String.class))).thenAnswer(iom -> {
            final JsonElement jsonElement = (JsonElement) iom.getArguments()[0];
            return jsonElement.getAsJsonPrimitive().getAsString();
        });
        final JsonParser parser = new JsonParser();
        try ( final JsonReader jsonReader = getPackageResourceJsonReader(ZonedDateTimeJsonDeserializerTest.class, "zoned-date-time.json") ) {
            final JsonElement jsonElement = parser.parse(jsonReader);
            final ZonedDateTime actualZonedDateTime = unit.deserialize(jsonElement, ZonedDateTime.class, mockContext);
            assertThat(actualZonedDateTime, is(expectedZonedDateTime));
        }
        verify(mockContext).deserialize(any(JsonPrimitive.class), eq(String.class));
        verifyNoMoreInteractions(mockContext);
    }

}

请注意,此处的每个测试都需要构建一些Gson配置,以便让反序列化上下文有效,或者后者必须被模拟。非常适合测试一个简单的单元。

Gson中JSON树模型的替代方案是面向流的类型适配器,它不需要构造整个JSON树,因此您可以轻松地直接读取或写入JSON流,从而实现(de)序列化更快,更少的内存消耗。特别是,对于简单的情况,比如琐碎的字符串&lt; ==&gt; FooBar转换。

final class ZonedDateTimeTypeAdapter
        extends TypeAdapter<ZonedDateTime> {

    private static final TypeAdapter<ZonedDateTime> zonedDateTimeTypeAdapter = new ZonedDateTimeTypeAdapter().nullSafe();

    private ZonedDateTimeTypeAdapter() {
    }

    static TypeAdapter<ZonedDateTime> getZonedDateTimeTypeAdapter() {
        return zonedDateTimeTypeAdapter;
    }

    @Override
    public void write(final JsonWriter out, final ZonedDateTime zonedDateTime) {
        throw new UnsupportedOperationException();
    }

    @Override
    public ZonedDateTime read(final JsonReader in)
            throws IOException {
        try {
            final String s = in.nextString();
            return ZonedDateTime.parse(s, customDateTimeFormatter);
        } catch ( final DateTimeParseException ex ) {
            throw new JsonParseException(ex);
        }
    }

}

以下是上述类型适配器的简单单元测试:

public final class ZonedDateTimeTypeAdapterTest {

    private static final ZonedDateTime expectedZonedDateTime = ZonedDateTime.of(2017, 3, 27, 14, 9, 47, 0, UTC);

    @Test(expected = UnsupportedOperationException.class)
    public void testWrite() {
        final TypeAdapter<ZonedDateTime> unit = getZonedDateTimeTypeAdapter();
        unit.toJsonTree(expectedZonedDateTime);
    }

    @Test
    public void testRead()
            throws IOException {
        final TypeAdapter<ZonedDateTime> unit = getZonedDateTimeTypeAdapter();
        try ( final Reader reader = getPackageResourceReader(ZonedDateTimeTypeAdapterTest.class, "zoned-date-time.json") ) {
            final ZonedDateTime actualZonedDateTime = unit.fromJson(reader);
            assertThat(actualZonedDateTime, is(expectedZonedDateTime));
        }
    }

}

对于简单的情况,我肯定会使用类型适配器,但它们可能有点难以实现。您还可以参考Gson unit tests获取更多信息。