Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test a custom JsonConverter

I have a json payload that I want to deserialize in a non-trivial way.

{
   "destinationId": 123
}

The target class is

public class SomeObject
{
    public Destination Destination { get; set; }
}

public class Destination
{
    public Destination(int destinationId)
    {
        Id = destinationId;
    }

    public int Id { get; set; }
}

To be able to do so, I've created a JsonConverter that takes care of it.

Here is the ReadJson method:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    if (CanConvert(objectType))
    {
        var value = reader.Value;

        if (value is long v)
        {
            // TODO: this might overflow
            return new Destination((int)v);
        }
    }

    return null;
}

I have then decorated the Destination class with a [JsonConverter] attribute accepting a typeof(DestinationConverter).

This works correctly when I use JsonConvert.DeserializeObject<SomeObject>(myString) (see unit test below) but I'm having issues creating a successful unit test for the JsonConverter specifically (see second test below).

[Test, AutoData]
public void SomeObject_is_correctly_deserialized(SomeObject testObject)
{
    var json = $@"{{""destinationId"":{testObject.Destination.Id}}}";

    Console.WriteLine($"json: {json}");

    var obj = JsonConvert.DeserializeObject<SomeObject>(json);

    Assert.That(obj.Destination.Id, Is.EqualTo(testObject.Destination.Id));
}

[Test, AutoData]
public void ReadJson_can_deserialize_an_integer_as_Destination(DestinationConverter sut, int testValue)
{
    JsonReader reader = new JTokenReader(JToken.Parse($"{testValue}"));

    var obj = sut.ReadJson(reader, typeof(Destination), null, JsonSerializer.CreateDefault());

    var result = obj as Destination;

    Assert.That(result, Is.Not.Null);
    Assert.That(result, Is.InstanceOf<Destination>());
    Assert.That(result.Id, Is.EqualTo(testValue));
}

I've been googling for a way to properly unit-test a converted but I only find examples of people using the whole DeserializeObject instead of just testing the converter.

PS: I pasted all the necessary code in a .NET Fiddle: https://dotnetfiddle.net/oUXi6k

like image 730
Kralizek Avatar asked Nov 06 '18 16:11

Kralizek


People also ask

How do I unit test a JSON converter?

Thus, to properly unit-test your converter, you need to advance the reader to the first token of the c# object you are trying to read, e.g. like so: JsonReader reader = new JsonTextReader(new StringReader(json)); while (reader. TokenType == JsonToken.

What is JsonConverter?

A converter is a class that converts an object or a value to and from JSON. The System. Text. Json namespace has built-in converters for most primitive types that map to JavaScript primitives.


1 Answers

Your basic problem is that, when you create a JsonReader, it is initially positioned before the first token. This is alluded to in the documentation for JsonToken:

JsonToken Enumeration

Specifies the type of JSON token.

Members

  • None: 0 This is returned by the JsonReader if a read method has not been called.

Thus, to properly unit-test your converter, you need to advance the reader to the first token of the c# object you are trying to read, e.g. like so:

JsonReader reader = new JsonTextReader(new StringReader(json));
while (reader.TokenType == JsonToken.None)
    if (!reader.Read())
        break;

var obj = sut.ReadJson(reader, typeof(Destination), null, JsonSerializer.CreateDefault());

Sample fiddle here.

Having done that, I would suggest you rewrite your converter as follows:

public class DestinationConverter : JsonConverter
{
    public override bool CanConvert(System.Type objectType)
    {
        return objectType == typeof(Destination);
    }

    public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
    {
        var id = serializer.Deserialize<int?>(reader);
        if (id == null)
            return null;
        return new Destination(id.Value);
    }

    public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
    {
        // WriteJson() is never called with a null value, instead Json.NET writes null automatically.
        writer.WriteValue(((Destination)value).Id);
    }
}

By calling serializer.Deserialize<int?>(reader) inside ReadJson(), you guarantee that:

  • null values are handled during reading.

  • An exception will get thrown in case of not-well-formed JSON (e.g. a truncated file).

  • An exception will get thrown in case of invalid JSON (e.g. an object where an integer was expected, or an integer overflow).

  • The reader will be correctly positioned at the end of the token being read. (In cases where the token is a primitive the reader does not need to be advanced, but for more complex tokens, it does.)

Sample fiddle #2 here.

You might also want to enhance your unit tests to check that:

  1. The reader is correctly positioned after ReadJson(), e.g. by asserting the the TokenType and Depth are correct, or even counting the number of tokens remaining in the JSON stream and asserting it is as expected.

    A common mistake when writing a converter is to leave the reader mis-positioned after conversion. When this is done, the object itself is read successfully but all subsequent objects become corrupt. Unit-testing ReadJson() directly will not catch this unless you assert that the reader is correctly positioned afterwards.

  2. An exception is thrown for a not-well-formed JSON stream, e.g. one that is truncated.

  3. An exception is thrown for an unexpected JSON token, e.g. when an array is encountered where a primitive is expected.

like image 169
dbc Avatar answered Sep 25 '22 21:09

dbc