Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 379
Anthony Brenelière Avatar asked May 03 '19 08:05

Anthony Brenelière


1 Answers

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.

like image 120
Jeroen Mostert Avatar answered Oct 28 '22 11:10

Jeroen Mostert