I had to unit test a method that calls a service which returns an interface type. This returned interface is then serialised into JSON string and stored into to some database.
This is an example of such method
public interface ISomeInterface { }
public interface ISomeService
{
ISomeInterface GetSomeInterface();
}
public class SampleService
{
private readonly ISomeService _someService;
public SampleService(ISomeService someService)
{
_someService = someService;
}
public void SomeMethod()
{
var result = _someService.GetSomeInterface();
var json = JsonConvert.SerializeObject(result);
}
}
This is the example of the unit test
[Test]
public void Test()
{
var mockResult = new Mock<ISomeInterface>();
var mockService = new Mock<ISomeService>();
mockService.Setup(x => x.GetSomeInterface())
.Returns(mockResult.Object).Verifiable();
var sut = new SampleService(mockService.Object);
sut.SomeMethod();
// Asserts goes here
}
I thought serialising any mock object to JSON should just work but it has self referencing exception as Mock object is quite complex.
To get this working, I added a custom JSON converter for Mock<T> and an extension method to register this custom converter if the mock object is expected to be serialised in the target method in the test.
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. Call '.RegisterForJsonSerialization()' after you do new Mock<T>()");
}
serializer.Serialize(writer, mockSerializer());
}
}
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.
public static class MockExtensions
{
public static Mock<T> RegisterForJsonSerialization<T>(this Mock<T> mock) where T : class
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new[] { new JsonMockConverter() }
};
mock.SetupAllProperties();
JsonMockConverter.RegisterMock(
mock,
() => typeof(T).GetProperties().ToDictionary(p => p.Name, p => p.GetValue(mock.Object))
);
return mock;
}
}
This method adds the JsonMockConverter to JSON default settings. It then automatically set up all properties of the mock and register the mock to JsonMockConverter. The Func<object> will return a dictionary of all properties of the type of the mock object and values of mock.Object.
This is the updated test to pass the test.
[Test]
public void Test()
{
var mockResult = new Mock<ISomeInterface>();
mockResult.RegisterForJsonSerialization();
var mockService = new Mock<ISomeService>();
mockService.Setup(x => x.GetSomeInterface())
.Returns(mockResult.Object).Verifiable();
var sut = new SampleService(mockService.Object);
sut.SomeMethod();
// Asserts goes here
}