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 GsonBuilder
as 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.
答案 0 :(得分:6)
JsonSerializer
和JsonDeserializer
紧密绑定到Gson JSON树模型和特定的Gson
配置(de)序列化上下文,它提供了一组可以(反)序列化的类型。因此,完成JsonSerializer
和JsonDeserializer
的单元测试相当困难。
请考虑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获取更多信息。