Felix Felix - 4 months ago 18
JSON Question

Unit test for a collection of anonymous JSON objects

This question was inspired by this excellent example. I have ASP.NET Core MVC application and I am writing unit tests for the controller. One of the methods returns

JsonResult
with a collection of anonymous types. I can get to each element of the collection. I can also assert values in each element like this:

Dictionary<int, string> expectedValues = new Dictionary<int, string> {
{ 1, "Welcome Tester"},
{ 2, "Namaste Tester"},
{ 3, "Privet Tester"},
{ 4, "Labdien Tester"}
};
foreach (dynamic value in jsonCollection) {
dynamic json = new DynamicObjectResultValue(value);
Assert.Equal(expectedValues[json.Id], json.Greeting);
}


But is there a way to make assertions on the whole collection? For example,
Assert.Equal(4, jsonCollection.Count())
or
Assert.Contains(2, jsonCollection[Id])
(this is obviously pseudo-code).

Answer

Here is an updated version of the dynamic object wrapper.

public static class DynamicObjectWrapperExtension {
    /// <summary>
    /// Return provided object as a <seealso cref="System.Dynamic.DynamicObject"/>
    /// </summary>  
    public static dynamic AsDynamicObject(this object value) {
        return new DynamicObjectWrapper(value);
    }
}

public class DynamicObjectWrapper : DynamicObject, IEquatable<DynamicObjectWrapper> {
    private readonly object value;
    private readonly Type valueType;

    public DynamicObjectWrapper(object value) {
        this.value = value;
        this.valueType = value.GetType();
    }

    public override IEnumerable<string> GetDynamicMemberNames() {
        return valueType.GetProperties().Select(p => p.Name);
    }

    public override bool TryConvert(ConvertBinder binder, out object result) {
        result = null;
        try {
            result = changeTypeCore(value, binder.Type);
        } catch {
            return false;
        }
        return true;
    }

    private object changeTypeCore(object value, Type convertionType) {
        if (ReferenceEquals(value, null))
            return getDefaultValueForType(convertionType);

        var providedType = valueType;
        if (convertionType.IsAssignableFrom(providedType)) {
            return value;
        }

        try {
            var converter = TypeDescriptor.GetConverter(convertionType);
            if (converter.CanConvertFrom(providedType)) {
                return converter.ConvertFrom(value);
            }

            converter = TypeDescriptor.GetConverter(providedType);
            if (converter.CanConvertTo(providedType)) {
                return converter.ConvertTo(value, convertionType);
            }
        } catch {
            return value;
        }

        try {
            return Convert.ChangeType(value, convertionType, System.Globalization.CultureInfo.CurrentCulture);
        } catch {
            return value;
        }
    }

    private object getDefaultValueForType(Type targetType) {
        return targetType.IsClass || targetType.IsInterface ? null : Activator.CreateInstance(targetType);
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) {
        result = null;
        //1d collection
        if (potentialIndex(indexes)) {
            int index = (int)indexes[0];
            var list = value as IList;
            if (validIndex(index, list)) {
                result = checkValue(list[index]);
                return true;
            }
        }
        return false;
    }

    private bool validIndex(int index, IList list) {
        return index >= 0 && index < list.Count;
    }

    private bool potentialIndex(object[] indexes) {
        return indexes[0] != null && typeof(int) == indexes[0].GetType() && value is IList;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result) {
        return TryGetValue(binder.Name, out result);
    }

    public bool TryGetValue(string propertyName, out object result) {
        result = null;
        var property = valueType.GetProperty(propertyName);
        if (property != null) {
            var propertyValue = property.GetValue(value, null);
            result = checkValue(propertyValue);
            return true;
        }
        return false;
    }

    private object checkValue(object value) {
        var valueType = value.GetType();
        return isAnonymousType(valueType)
            ? new DynamicObjectWrapper(value)
            : value;
    }

    private bool isAnonymousType(Type type) {
        //HACK: temporary hack till a proper function can be implemented
        return type.Namespace == null &&
            type.IsGenericType &&
            type.IsClass &&
            type.IsSealed &&
            type.IsPublic == false;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) {
        try {
            result = valueType.InvokeMember(
                binder.Name,
                BindingFlags.InvokeMethod |
                BindingFlags.Public |
                BindingFlags.Instance,
                null, value, args);

            return true;
        } catch {
            result = null;
            return false;
        }
    }

    public override bool Equals(object obj) {
        // If parameter is null return false.
        if (ReferenceEquals(obj, null)) return false;

        // Return true if the fields match:
        return this.value == obj || (obj is DynamicObjectWrapper && Equals(obj as DynamicObjectWrapper));
    }

    public bool Equals(DynamicObjectWrapper other) {
        // If parameter is null return false.
        if (ReferenceEquals(other, null)) return false;
        // Return true if the fields match:
        return this.value == other.value;
    }

    public override int GetHashCode() {
        return ToString().GetHashCode();
    }

    public override string ToString() {
        var name = GetType().Name;
        return string.Format("{0}[{1}]", name, value);
    }

}

Assuming the following controller

public class FooController : Controller {

    public IActionResult GetAnonymousObject() {

        var jsonResult = new {
            id = 1,
            name = "Foo",
            type = "Bar"
        };

        return Json(jsonResult);
    }

    public IActionResult GetAnonymousCollection() {

        var jsonResult = Enumerable.Range(1, 20).Select(x => new {
            id = x,
            name = "Foo" + x,
            type = "Bar" + x
        }).ToList();

        return Json(jsonResult);
    }
}

Usage examples

[TestClass]
public class DynamicObjectWrapperTests {
    [TestMethod]
    public void DynamicObjectResultValue_Member_Should_Exist() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousObject() as JsonResult;

        //Assert
        dynamic obj = result.Value.AsDynamicObject();

        Assert.IsNotNull(obj);
        Assert.AreEqual(1, obj.id);
        Assert.AreEqual("Foo", obj.name);
        Assert.AreEqual(3, obj.name.Length);
        Assert.AreEqual("Bar", obj.type);
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;

        //Assert
        dynamic jsonCollection = result.Value;
        foreach (object value in jsonCollection) {
            dynamic json = value.AsDynamicObject();

            Assert.IsNotNull(json.id,
                "JSON record does not contain \"id\" required property.");
            Assert.IsNotNull(json.name,
                "JSON record does not contain \"name\" required property.");
            Assert.IsNotNull(json.type,
                "JSON record does not contain \"type\" required property.");
        }
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Should_Convert_To_IEnumerable() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;
        dynamic jsonCollection = result.Value.AsDynamicObject();
        int count = 0;
        foreach (var value in jsonCollection) {
            count++;
        }

        //Assert
        Assert.IsTrue(count > 0);
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Index_at_0_Should_Not_be_Null() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;
        dynamic jsonCollection = result.Value.AsDynamicObject();

        //Assert                
        Assert.IsNotNull(jsonCollection[0]);
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Should_Be_Indexable() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;
        dynamic jsonCollection = result.Value.AsDynamicObject();

        //Assert
        for (var i = 0; i < jsonCollection.Count; i++) {

            var json = jsonCollection[i];

            Assert.IsNotNull(json);
            Assert.IsNotNull(json.id,
               "JSON record does not contain \"id\" required property.");
            Assert.IsNotNull(json.name,
                "JSON record does not contain \"name\" required property.");
            Assert.IsNotNull(json.type,
                "JSON record does not contain \"type\" required property.");

        }
    }

    [TestMethod]
    public void DynamicObjectResultValue_DynamicCollection_Count_Should_Be_20() {
        //Arrange
        var controller = new FooController();

        //Act
        var result = controller.GetAnonymousCollection() as JsonResult;

        //Assert
        dynamic jsonCollection = result.Value.AsDynamicObject();

        Assert.AreEqual(20, jsonCollection.Count);
    }

}