Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How Mock JsonReader unit testing a custom JsonConverter

Wrote a Custom JsonConverter to handle different Json formats that are returned by different versions of the same api. One app makes a request to several other apps, and we dont know which format will be returned so the JsonConverter handles this and seems to work well. I need to add unit tests to the project, except I have not found helpful resources to help Mock out some of the Newtonsoft.Json objects, mainly JsonReader.

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jsonValue = JObject.Load(reader);
        if(jsonValue == null)
        {
            return null;
        }

        var responseData = ReadJsonObject(jsonValue);
        return responseData;
    }

    [TestMethod]
    public void ReadJsonReturnNullForNullJson()
    {
        var converter = new DataConverter();

        _mockJsonReader.Setup(x => x.Value).Returns(null);

        var responseData = converter.ReadJson(_mockJsonReader.Object, typeof(ProbeResponseData), null, _mockJsonSerializer.Object);

        Assert.IsNull(responseData);
    }

Some code has been taken out of the ReadJson method. I am trying Setup the JsonReader to return the value of the actual json, in this case a null value but in other unit tests I would want an actual Json(JObject). When running the unit test I receive a "Newtonsoft.JsonReaderException: Error reading JObject from JsonReader. Path ''."

like image 715
Casey O'Brien Avatar asked Oct 19 '17 17:10

Casey O'Brien


People also ask

Can I mock elements such as jsonconvert or jsonserializer?

Going further, I could potentially mock elements, such as the JsonReader and JsonSerializer to result in different preconditions so I can test a wide array of scenarios. The issue with relying on JsonConvert or JsonSerializer to run the full deserialization process, is that you're introducing other logic which is largely outside of your control.

How does the jsonmockconverter work?

Basically the JsonMockConverter has 2 lists. One to keep track of the type of Mock object and one to keep a Func<object> which returns a custom object to be serialised for the type in the first list respectively. The converter has a method RegisterMock<T> which allows you to register how you want your Mock object to be serialised.

How do I set the default JSON converter for a class?

Apply the [JsonConverter] attribute to a class or a struct that represents a custom value type. Here's an example that makes the DateTimeOffsetJsonConverter the default for properties of type DateTimeOffset: Suppose you serialize an instance of the following type: Here's an example of JSON output that shows the custom converter was used:

How do I use a custom converter with jsonserializer?

To use this custom converter, you add it to JsonSerializarOptions.Converters, then pass the options in when you’re using JsonSerializer, like this: When JsonSerializer encounters a property of the type that your custom converter handles, it’ll delegate serialization to your converter.


2 Answers

The use of DeserializeObject<T> will call your override of ReadJson under the hood.

[TestMethod]
public void ReadJsonVerifyTypeReturned()
{
    var testJson = CreateJsonString();

    var result = JsonConvert.DeserializeObject<ProbeResponseData>(testJson);
    var resultCheck = result as ProbeResponseData;

    Assert.IsNotNull(resultCheck);
}
like image 195
Jeff Dalley Avatar answered Oct 28 '22 09:10

Jeff Dalley


Whilst using JsonConvert or JsonSerializer directly will allow you to test it, you probably should make your converter tests a little more direct. For instance, you can't guarantee that JSON.NET will do what you expect when you call the deserializer, whereas what you actually want to test is your custom converter - what JSON.NET does with that is out of your control.

Consider this example:

public readonly struct UserId
{
  public static readonly UserId Empty = new UserId();

  public UserId(int value)
  {
    Value = value;
    HasValue = true;
  }

  public int Value { get; }
  public bool HasValue { get; }
}

I've got this struct, which is backed by an int. I want to deserialize a specific JSON number value as an int -> UserId. So, I create a custom converter:

public class UserIdConverter
{
  public override bool CanConvert(Type objectType) => objectType == typeof(UserId);

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    int? id = serializer.Deserialize<int?>(reader);
    if (!id.HasValue)
    {
      return UserId.Empty;
    }

    return new UserId(id.Value);
  }
}

I've skipped over the implementation of WriteJson in this instance, but the logic is the same.

I would write my test as follows:

[Fact]
public void UserIdJsonConverter_CanConvertFromJsonNumber()
{
    // Arrange
    var serialiser = new JsonSerializer();
    var reader = CreateJsonReader("10");
    var converter = new UserIdJsonConverter();

    // Act
    var result = converter.ReadJson(reader, typeof(UserId), null, serialiser);

    // Assert
    Assert.NotNull(result);
    Assert.IsType<UserId>(result);

    var id = (UserId)result;
    Assert.True(id.HasValue);
    Assert.Equal(10, id.Value);
}

private JsonTextReader CreateJsonReader(string json)
    => new JsonTextReader(new StringReader(json));

In doing so, I can create a test purely around my ReadJson method, and confirm it does what I expect. Going further, I could potentially mock elements, such as the JsonReader and JsonSerializer to result in different preconditions so I can test a wide array of scenarios.

The issue with relying on JsonConvert or JsonSerializer to run the full deserialization process, is that you're introducing other logic which is largely outside of your control. I.e., what if through deserialization, JSON.NET actually makes a different decision and your custom converter is never used - your test isn't responsible for testing JSON.NET itself, but what your custom converter actually does.

like image 39
Matthew Abbott Avatar answered Oct 28 '22 08:10

Matthew Abbott