简体   繁体   中英

How to mock (with Moq) an interface that is serialized by Newtonsoft.json?

I mock an interface from which the object is serialized by the tested code by the Newtonsoft JsonConvert.SerializeObject.

The serializer throws an exception with the following error:

Newtonsoft.Json.JsonSerializationException : Self referencing loop detected for property 'Object' with type 'Castle.Proxies.IActionProxy'. Path 'Mock '.

Newtonsoft tries to serialized the proxy object IActionProxy, that has a property Mock that loops on that serialized object.

Curiously changing serializer options

ReferenceLoopHandling = ReferenceLoopHandling.Serialize ( ot Ignore)
PreserveReferencesHandling = PreserveReferencesHandling.All

..does not solve the problem, serialization become infinite

Thank you for help about this issue, I would be happy to have a way of using Moq in that case

UPDATE : here is a sample code to produce the exception:

Mock<IAction> _actionMock = new Mock<IAction>().SetupAllProperties();
Newtonsoft.Json.JsonConvert.SerializeObject( _actionMock.Object );  // JsonSerializationException (this line is in a method which i'm not responsible of )
// IAction is any interface with some properties

We have to consider the serialization (SerializeObject) is called by the tested code in a library I don't have access to.

This is a little rough around the edges, but it does the job:

public class JsonMockConverter : JsonConverter {
    static readonly Dictionary<object, Func<object>> mockSerializers = new Dictionary<object, Func<object>>();
    static readonly HashSet<Type> mockTypes = new HashSet<Type>();

    public static void RegisterMock<T>(Mock<T> mock, Func<object> serializer) where T : class {
        mockSerializers[mock.Object] = serializer;
        mockTypes.Add(mock.Object.GetType());
    }

    public override bool CanConvert(Type objectType) => mockTypes.Contains(objectType);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => 
        throw new NotImplementedException();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
        if (!mockSerializers.TryGetValue(value, out var mockSerializer)) {
            throw new InvalidOperationException("Attempt to serialize unregistered mock.");
        }
        serializer.Serialize(writer, mockSerializer());
    }
}

A little extension method for ease of use:

internal static class MockExtensions {
    public static Mock<T> RegisterForJsonSerialization<T>(this Mock<T> mock) where T : class {
        JsonMockConverter.RegisterMock(
            mock, 
            () => typeof(T).GetProperties().ToDictionary(p => p.Name, p => p.GetValue(mock.Object))
        );
        return mock;
    }
}

Set up as follows:

JsonConvert.DefaultSettings = () => new JsonSerializerSettings {
    Converters = new[] { new JsonMockConverter() }
};

And now the following code works:

public interface IAction {
    int IntProperty { get; set; }
}

var actionMock = new Mock<IAction>()
    .SetupAllProperties()
    .RegisterForJsonSerialization();

var action = actionMock.Object;
action.IntProperty = 42;

Console.WriteLine(JsonConvert.SerializeObject(action));

Making it so that you don't have to register your mocks for serialization is much harder -- there is no robust way of figuring out that an object is a mock, and if it is, what type it's supposed to be mocking, and if it is , how we're supposed to serialize that using the mock's data. This could only be done with some really nasty and brittle reflection over Moq's internals, but let's not go there. It could possibly be added to Moq itself as a feature, though.

This can be further extended with custom ways of serializing -- here I've assumed we're OK with just serializing the public properties of the mocked type. This is a little naive -- it will not work correctly if you are inheriting interfaces, for example, because Type.GetProperties only gets the properties declared on the interface itself. Fixing that, if desired, is left as an exercise to the reader.

Extending this for deserialization is possible in principle, but a bit trickier. It would be very unusual to need that for mocking purposes, as opposed to a concrete instance.

如果您不担心 JsonConvert.SerializeObject() 结果,您可以简单地将以下行添加到您的代码中:

_actionMock.As<ISerializable>();

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