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 ''."
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.
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.
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:
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.
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);
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With