如何对自定义 JsonConverter 进行单元测试

How to unit test a custom JsonConverter

我有一个 json 负载,我想以一种非平凡的方式反序列化。

{
   "destinationId": 123
}

目标class是

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

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

    public int Id { get; set; }
}

为了能够做到这一点,我创建了一个 JsonConverter 来处理它。

这是 ReadJson 方法:

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

然后我用接受 typeof(DestinationConverter).

[JsonConverter] 属性修饰了 Destination class

这在我使用 JsonConvert.DeserializeObject<SomeObject>(myString) 时工作正常(请参阅下面的单元测试)但是我在为 JsonConverter 创建成功的单元测试时遇到问题(请参阅下面的第二个测试)。

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

我一直在谷歌上寻找一种方法来正确地对转换后的单元进行测试,但我只找到了人们使用整个 DeserializeObject 而不是仅仅测试转换器的示例。

PS:我在 .NET 中粘贴了所有必要的代码 Fiddle:https://dotnetfiddle.net/oUXi6k

您的基本问题是,当您创建 JsonReader 时,它最初位于第一个标记 之前 之前。 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.

因此,为了正确地对您的转换器进行单元测试,您需要将 reader 推进到您尝试读取的 c# 对象的第一个标记,例如像这样:

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

示例 fiddle here.

完成后,我建议您按如下方式重写转换器:

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

通过在 ReadJson() 中调用 serializer.Deserialize<int?>(reader),您保证:

  • null 值在读取期间处理。

  • 如果格式不正确 JSON(例如截断的文件),将引发异常。

  • 如果 JSON 无效(例如,需要整数或整数溢出的对象),将引发异常。

  • reader 将正确定位在正在读取的标记的末尾。 (在令牌是原始令牌的情况下,不需要推进 reader,但对于更复杂的令牌,它需要。)

示例 fiddle #2 here.

您可能还想增强单元测试以检查:

  1. reader 正确定位在 ReadJson() 之后,例如通过断言 TokenType and Depth 是正确的,或者甚至计算 JSON 流中剩余的令牌数量并断言它符合预期。

    编写转换器时的一个常见错误是在转换后将 reader 放错位置。完成后,对象 自身 被成功读取,但所有 后续 对象都已损坏。直接单元测试 ReadJson() 不会捕捉到这一点,除非您断言 reader 之后被正确定位。

  2. 对于格式不正确的 JSON 流抛出异常,例如一个被截断了。

  3. 对于意外的 JSON 标记抛出异常,例如当在需要原语的地方遇到数组时。