Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mock.Of or Mock.From function to generate mock from anonymous object #152

Closed
sergeyt opened this issue Feb 4, 2015 · 3 comments
Closed

Comments

@sergeyt
Copy link

sergeyt commented Feb 4, 2015

Consider to include to Moq code base a function to generate mock from anonymous object. I wrote the following working prototype.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Tests
{
    using PropertyValue = KeyValuePair<string,object>;

    /// <summary>
    /// Builds <see cref="Mock<T>"/> from anonymous object.
    /// </summary>
    internal class AnonyMock
    {
        /// <summary>
        /// Builds mock for given type from anonymous object.
        /// </summary>
        /// <typeparam name="T">The interface to mock.</typeparam>
        /// <param name="obj">Anonymous object.</param>
        /// <returns></returns>
        public static T Of<T>(object obj) where T:class
        {
            Debug.Assert(IsAnon(obj));
            return Mock.Of(MakePredicate<T>(obj));
        }

        private static Expression<Func<T, bool>> MakePredicate<T>(object obj)
        {
            var type = typeof(T);
            var parameter = Expression.Parameter(typeof(T), "t");
            var props = (
                from p in obj.GetType().GetProperties()
                where p.Name != "type"
                select PropertyPredicate(parameter, obj, type, p)
                ).ToArray();
            return Expression.Lambda<Func<T, bool>>(props.Skip(1).Aggregate(props[0], Expression.AndAlso), parameter);
        }

        private static Expression PropertyPredicate(Expression parameter, object instance, Type targetType, PropertyInfo pi)
        {
            var value = pi.GetValue(instance, null);

                        // example of composite properties
            Func<object, IEnumerable<PropertyValue>> destruct;
            if (CompositeProperties.TryGetValue(pi.Name, out destruct))
            {
                var props = (from p in destruct(value) select PropertyEqual(parameter, targetType, p.Key, p.Value)).ToArray();
                return props.Skip(1).Aggregate(props[0], Expression.AndAlso);
            }

            var targetProperty = ResolveProperty(targetType, pi.Name);
            Debug.Assert(targetProperty != null);

            // support T[], IEnumerable<T>, IItemCollection<T>
            var array = value as Array;
            if (array != null)
            {
                // replace anonymous objects
                var itemType = GetItemType(targetProperty.PropertyType);
                var items = (from it in array.Cast<object>() select MockIfAnon(it, itemType)).ToArray();

                var targetArray = Array.CreateInstance(itemType, items.Length);
                for (var i = 0; i < items.Length; i++)
                {
                    targetArray.SetValue(items[i], i);
                }

                value = targetArray;

                if (targetProperty.PropertyType.GetGenericTypeDefinition() == typeof(IItemCollection<>))
                {
                    value = CreateItemCollection(targetArray, itemType);
                }
            }

            return Expression.Equal(Expression.Property(parameter, targetProperty), Expression.Constant(value));
        }

        private static object MockIfAnon(object obj, Type type)
        {
            if (!IsAnon(obj)) return obj;

            var typeProperty = obj.GetType().GetProperty("type");
            if (typeProperty != null)
            {
                type = (Type) typeProperty.GetValue(obj, null);
            }

            var method = typeof(Amock).GetMethod("Of").MakeGenericMethod(type);
            return method.Invoke(null, new[] { obj });
        }

        private static Expression PropertyEqual(Expression target, Type declType, string name, object value)
        {
            var prop = GetProperty(target, declType, name);
            return Expression.Equal(prop, Expression.Constant(value));
        }

        private static Expression GetProperty(Expression target, Type declType, string name)
        {
            return Expression.Property(target, ResolveProperty(declType, name));
        }

        private static PropertyInfo ResolveProperty(Type type, string name)
        {
            // TODO cache properties
            var types = new[] {type}.Concat(BaseTypes(type)).Concat(type.GetInterfaces()).ToArray();
            return types.Select(t => t.GetProperty(name)).FirstOrDefault(x => x != null);
        }

        private static IEnumerable<Type> BaseTypes(Type type)
        {
            while (type != null && type.BaseType != null)
            {
                yield return type.BaseType;
                type = type.BaseType;
            }
        }

        private static bool IsAnon(object value)
        {
            return value != null && CheckIfAnonymousType(value.GetType());
        }

        private static bool CheckIfAnonymousType(Type type)
        {
            if (type == null)
                throw new ArgumentNullException("type");

            // HACK: The only way to detect anonymous types right now.
            return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
                && type.IsGenericType && type.Name.Contains("AnonymousType")
                && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
                && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
        }

        private static Type GetItemType(Type type)
        {
            if (type == null) return null;

            if (type.IsArray) return type.GetElementType();

            // IEnumerable<T>
            var i = FindIEnumerableT(type);
            if (i != null) return i.GetGenericArguments()[0];

            // support non-generic collections like old .NET 1 collections based on CollectionBase
            if (!typeof(IEnumerable).IsAssignableFrom(type))
                return null;

            var add = type.GetMethod("Add");
            if (add == null)
                return null;

            var parameters = add.GetParameters();
            if (parameters.Length != 1)
                return null;

            return parameters[0].ParameterType;
        }

        private static Type FindIEnumerableT(Type type)
        {
            if (type == null || type == typeof(string))
                return null;

            if (type.IsArray)
                return typeof(IEnumerable<>).MakeGenericType(type.GetElementType());

            if (type.IsGenericType)
            {
                var ienum = type.GetGenericArguments()
                                .Select(x => typeof(IEnumerable<>).MakeGenericType(x))
                                .FirstOrDefault(x => x.IsAssignableFrom(type));
                if (ienum != null)
                {
                    return ienum;
                }
            }

            var ifaces = type.GetInterfaces();
            if (ifaces.Length > 0)
            {
                var ienum = ifaces.Select(x => FindIEnumerableT(x)).FirstOrDefault(x => x != null);
                if (ienum != null)
                {
                    return ienum;
                }
            }

            if (type.BaseType != null && type.BaseType != typeof(object))
            {
                return FindIEnumerableT(type.BaseType);
            }

            return null;
        }

        private static object CreateItemCollection(Array items, Type itemType)
        {
            var method = typeof(Amock).GetMethod("ItemCollection").MakeGenericMethod(itemType);
            return method.Invoke(null, new[] {items});
        }

        public static IItemCollection<T> ItemCollection<T>(params T[] items)
        {
            return items.ToItemCollection();
        }
    }
}

Usage

namespace Tests
{
    [TestFixture]
    public class AnonyMockTests
    {
        [Test]
        public void TextBox()
        {
            var box = AnonyMock.Of<ITextBox>(new
            {
                Name = "a",
                Size = "1in,0.5in"
            });
            Assert.AreEqual("a", box.Name);
            Assert.AreEqual((Length)"1in", box.Width);
            Assert.AreEqual((Length)"0.5in", box.Height);
        }
    }
}
@gsscoder
Copy link

@sergeyt, I like it: it would be useful to see it implemented directly in Moq. Why you don't submit a PR for this?

@stakx
Copy link
Contributor

stakx commented Jun 21, 2017

var box = AnonyMock.Of<ITextBox>(new
           {
                Name = "a",
                Size = "1in,0.5in"
           });

I am not sure I fully understand the intended meaning of this. Would it be equivalent to the following?:

var box = Mock.Of<ITextBox>(b => b.Name == "a" && b.Size = "1in,0.5in");

@stakx
Copy link
Contributor

stakx commented Oct 8, 2017

Is anyone still interested in this new feature?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants