【问题标题】:Testing an isolated custom JsonDeserializer in Java在 Java 中测试一个隔离的自定义 JsonDeserializer
【发布时间】:2017-04-06 16:54:17
【问题描述】:

所以对于我正在编写的这个小程序,我正在寻找解析 Twitter 的推文流。我使用了很好的 Gson 库。 Gson 无法解析 Twitters created_at datetime 字段,所以我只好写了一个自定义的JsonDserializer,需要通过GsonBuilder注册到解析器,如下:

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

现在我的反序列化器运行良好,我能够解析 Twitter 的流。

但是,我试图用单元测试覆盖我的程序,所以应该包含这个自定义反序列化器。

由于一个好的单元测试是一个很好的隔离测试,我不想用Gson 对象注册它,然后我会解析一个json字符串。我想要的是创建我的反序列化器的一个实例,并只传递一个表示日期时间的通用字符串,这样我就可以在不与其他任何东西集成的情况下测试反序列化器。

JsonDeserializer 的反序列化方法的签名如下:

deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext)

假设我要解析以下数据:'Mon Mar 27 14:09:47 +0000 2017'。我必须如何转换输入数据才能正确测试我的反序列化器。

我不是在寻找实际解析此日期的代码,我已经涵盖了该部分。我在问如何满足deserialize 方法的签名,以便我可以模拟它在Gson 中的使用。

【问题讨论】:

  • 也许您应该间接测试它,作为Gson 实例的一部分?
  • 我不想间接测试它,因为我必须 100% 信任这个库(理论上我不能)。如果测试可能失败,那么由于反序列化器、Gson 库或反序列化器在 Gson 中的集成,“不可能”保证单元测试失败。这就是为什么我觉得我应该至少在一些测试中隔离反序列化器。

标签: java json serialization gson


【解决方案1】:

JsonSerializerJsonDeserializer 与 Gson JSON 树模型和特定的Gson 配置(反)序列化上下文紧密绑定,该上下文提供了一组可以(反)序列化的类型。正因为如此,完成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);
        }
    }

}

请注意,我通过上下文对字符串进行反序列化 intention 以强调更复杂的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 流,从而进行(反)序列化更快,更少的内存消耗。特别是对于简单的情况,例如简单的字符串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 了解更多信息。

【讨论】:

  • 我发现您的回答非常有用,我感谢您竭尽全力让事情变得清晰。我将尝试以您展示的方式模拟上下文,但我还将研究 TypeAdapters。我正在解析更大的 json 结构,而不仅仅是简单的日期时间,但同时它们只是简单的推文,这些 TypeAdapter 可能只是按照我想要的方式工作。干杯!
  • 非常好的答案!特别是因为您花时间提及/解释了流式处理方法。非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-10
  • 1970-01-01
相关资源
最近更新 更多