66

I've written custom JsonSerializer and JsonDeserializer for my app. Now I want to write some unit-tests for them.

How should a clean test case look like?

Are there some clean examples out there?

(clean means no dependencies to other frameworks or libraries)

gue
  • 1,099
  • 2
  • 9
  • 18

6 Answers6

70

JsonSerializer

The example is serialising a LocalDateTime but this can replaced by the required type.

@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

The example is deserialising a Number but this can replaced by the required type.

    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);
    }

UPDATE

When upgrading to jackson 2.9.3 I received a NullPointerException in DeserializationContext in isEnabled(MapperFeature feature) when deserialising strings to numbers because new ObjectMapper() initialises _config to null.

To get around this, I used this SO answer to spy on the final class DeserializationContext:

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

I feel there must be a better way, so please comment if you have one.

Robert Bain
  • 9,113
  • 8
  • 44
  • 63
  • 5
    For those who like me wondering what is `parser.nextToken` repeated three times here- What that means is we are trying to move parser to the value `1.1` from json `{value:1.1}`. `parser.nextToken` is required three times because the order is START_OBJECT, FIELD_NAME, VALUE_STRING and we want to reach VALUE_STRING in the example – nilesh Dec 28 '16 at 23:29
  • @nilesh : Is there a way to avoid repetition of parser.nextToken 3 times? I have similar requirement but dont want to use parser.nextToken 3 times – kedar Jan 24 '17 at 19:38
  • I looked at the [JsonParser API](http://fasterxml.github.io/jackson-core/javadoc/2.1.0/com/fasterxml/jackson/core/JsonParser.html?is-external=true) and [JsonParser source](https://github.com/FasterXML/jackson-core/blob/master/src/main/java/com/fasterxml/jackson/core/JsonParser.java) and I don't believe that there is. – Robert Bain Jan 24 '17 at 20:44
  • I don't think there is a way to avoid parse token reparation either – nilesh Jan 25 '17 at 06:48
  • @RobertBain Thanks for this. I struggled with getting it working but used this as a basis for coming up with my solution. See my answer below. – DearVolt Aug 26 '18 at 15:54
  • 1
    I also noticed there's no longer a need for the `InputStream`. One can simply use `mapper.getFactory().createParser(json)` – DearVolt Aug 26 '18 at 15:55
  • @RobertBain, Thanks your answer. Can we substitute three class to parser.nextToken() with any thing else. – Simsons Jul 22 '20 at 23:55
  • For those that can't get this answer to work, double check that you do call `jsonGenerator.flush()` as per the answer. I forgot to call it so my JSON was incomplete – xlm Dec 08 '21 at 01:02
30

Other answers have covered serialization very well. I have one other suggestion for deserialization.

If your domain class has the @JsonDeserialize annotation, then your deserializer will be picked up by the default ObjectMapper, without the need to explicitly register it.

@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;
    }
}

and your unit test can become much shorter:

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());
    }
}
bonh
  • 2,863
  • 2
  • 33
  • 37
19

A deserializer can be unit tested like this:

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"));
    }
    
}   
Sebastian D'Agostino
  • 1,575
  • 2
  • 27
  • 44
Emil Lunde
  • 291
  • 2
  • 4
  • There is a missing bracket ) at the end of the line before semicolon: assertThat(foo.getSomeProperty(), is("valueOfSomeProperty"); I cannot edit as it is only 1 character :-). – Seweryn Habdank-Wojewódzki May 23 '17 at 15:22
4

I don't find any example but you can try to create a generator like :

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

and you can get a SerializerProvider instance with

new ObjectMapper().getSerializerProvider();
JJD
  • 50,076
  • 60
  • 203
  • 339
herau
  • 1,466
  • 2
  • 18
  • 36
  • ...and we can execute 'stringJson.close(); stringJson.toString();' to get the json generated as string :) – Ismael Sarmento Jul 31 '20 at 18:58
  • FWIW `StringWriter.close()` doesn't do anything per [docs](https://docs.oracle.com/javase/7/docs/api/java/io/StringWriter.html#close()) but makes sense to close it nonetheless as it implements `Closeable` and `Autocloseable` – xlm Dec 08 '21 at 00:53
3

I found a way to unit test the Deserializers, was quite some trouble to figure out. Have a look at my repo https://bitbucket.org/arbeitsgruppedenktmit/de.denktmit.rest.hal

The unit test class is here: de.denktmit.rest.hal / src / test / java / de / denktmit / rest / hal / jackson / RelationDeserializerTest.java

An integration test, for testing the Deserializer in context can be found here: de.denktmit.rest.hal / src / test / java / de / denktmit / rest / hal / OrderResourceIntegrationTest.java

EDIT due to comment Base class to setup mapper for easy unit testing

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('.', '/');
    }
}

The test class containing the unit tests

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());
    }

}

The class under test

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));
    }


}

Hope that helps and best regards

JJD
  • 50,076
  • 60
  • 203
  • 339
Marius Schmidt
  • 633
  • 6
  • 18
-3

I started using the solution in @RobertBain's answer but it didn't work for me as the JSON parser kept losing the value somehow. I ended up including Mockito and stubbing the method on the JsonParser that my deserializer uses and wrote my tests using that. See a small example below:

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);
    }
}
DearVolt
  • 358
  • 3
  • 13