简体   繁体   中英

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 ''."

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

Without knowing the inner workings of your custom converter, could you just create a test double instead of a mock?

public class JsonReaderThatReturnsNull : JsonReader
{
    public override bool Read()
    {
        return true;
    }

    public override object Value => null;
}

To get to test the converter, it needs to be in the list of converters. If you used JsonConverterAttribute , it will work automatigically, but if you didn't you can do it like this:

var serializerSettings = new JsonSerializerSettings();
serializerSettings.Converters.Add(new MyJsonConverter());

var serializer = JsonSerializer.Create(serializerSettings);

var jObject = JObject.Parse(myString);
var result = jObject.ToObject<MyObject>(serializer);

Try to use this to check converted result.

DeserializedType result = JsonConvert.DeserializeObject<DeserializedType>(json, new Converter(parms));

From Documentation

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. Ie, 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM