5

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).

Community
  • 1
  • 1
Felix
  • 9,248
  • 10
  • 57
  • 89
  • `.Count()` is an extension method. Don't think members can be accessed/invoked if it does not exist directly on the wrapped object. If the object is of `ICollection` then `Count` property can be used. The other request is possible via overriding `TryGetIndex` and some type matching. – Nkosi Jul 24 '16 at 11:02

2 Answers2

4

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

}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Hmm.... It looks like Type.IsClass and Type.IsInterface are not available in ASP.NET Core. Should be type.GetTypeInfo().IsClass. http://stackoverflow.com/questions/35439749/how-to-check-if-a-type-is-abstract-in-net-core Still trying to figure out how to invoke InvokeMember() – Felix Jul 24 '16 at 20:27
  • Where are you trying to use Type.IsInterface? Also how and what member are you trying to invoke? The trickiest part so far is the isAnonymous method. – Nkosi Jul 24 '16 at 20:32
  • Nkosi for now I am just trying to compile your dynamic object wrapper ;) This is what I found re: InvokeMember https://github.com/npgsql/npgsql/issues/471 : InvokeMember -> No easy equivalent here, you’ll want to find the member and use appropriate invocation method based off BindingFlags values used in original call (varies by type of member, generally not very hard.) - I guess, easy for Microsoft guy to say :) – Felix Jul 24 '16 at 20:34
  • 1
    Ok well for now I would suggest removing the invoke member code given that the wrapped objects would be dtos. I had put it there when investigating the extension method. You should be able to remove it with no side effects. – Nkosi Jul 24 '16 at 20:58
  • Did that. Everything compiles and runs fine - but last two methods fail for jsonCollection: `DynamicObjectWrapper' does not contain a definition for 'Count'` – Felix Jul 24 '16 at 21:03
  • Are you using `Count` as a property or a method ie `Count()`. – Nkosi Jul 24 '16 at 21:04
  • tried both property and method - same error message – Felix Jul 24 '16 at 21:07
  • 1
    Ok investigating. You are running my test examples right? They all pass when I tested them – Nkosi Jul 24 '16 at 21:08
  • They work because the underlying type returned from controller is a `List` which has a `Count` property. If you are testing with your own collection type check to see what it uses. if its an array try length – Nkosi Jul 24 '16 at 21:14
  • 1
    Ok reproduced the same exception you got by changing the return collect to an array and not editing the test case. So I would suggest you check the expected type being return by the `JsonResult.Value`. – Nkosi Jul 24 '16 at 21:31
  • +10 This explains everything... The controller just returned `Json(IQueryable)` (didn't even realize that it would work!). By changing to `.ToList()`, `Count` got defined. Can't thank you enough! – Felix Jul 24 '16 at 21:49
  • @Nkosi, this is a great solution. However, with .Net Core 1.1 the Type.IsClass and Type.IsInterface properties are not available. As Felix pointed out a GetTypeInfo() is missing. Would you mind updating your code, since the question is about (ASP) .Net Core? – Christoph May 02 '17 at 11:20
0

In case simple assert and Linq queries are not enough, you can use CollectionAssert methods from mstest.

  • see more discussion on this thread
Community
  • 1
  • 1
robi-y
  • 1,687
  • 16
  • 25