如何通过给定参数自动设置MOQ并导致对象[]?

时间:2014-03-07 05:18:33

标签: moq

我想通过包含预期结果和参数的给定数组自动化安装代码。 像数据驱动的设置。

我现有的代码:

var mock = new Mock<ILogin>();
var testDataTable = new object[,]
{
    { LoginResult.Success, "Jack", "123!@#"}
    , { LoginResult.WrongPassword, "Jack", "123321"}
    , { LoginResult.NoSuchUser, "Peter", "123!@#"}
};


// ForEachRow is my own extension method
testDataTable.ForEachRow((row) =>
{
    var result = (LoginResult)row[0];
    var username = (string)row[1];
    var password = (string)row[2];

    mock.Setup(o => o.Login(
        It.Is<string>(u => u == username),
        It.Is<string>(p => p == password)
        )).Returns(result);
});

return mock.Object;

我希望的代码:

var mock = new Mock<ILogin>();
new object[,]
{
    { LoginResult.Success, "Jack", "123!@#"}
    , { LoginResult.WrongPassword, "Jack", "123321"}
    , { LoginResult.NoSuchUser, "Peter", "123!@#"}
}.ForEachRow((row) =>
{
    var exprTree = (ILogin o)=>o.Login("ANY", "ANY");
    AutoSetup(mock, exprTree, row);  // <---- How to write this AutoSetup?
});

return mock.Object;

如何编写上面的AutoSetup(mock,exprTree,dataArray)函数? 它需要三个参数:

  • mock:一个模拟对象,例如新模拟()
  • exprTree:表示要设置的方法的表达式树
  • dataArray:对象[],0元素是预期结果,其他是传递给方法的参数

1 个答案:

答案 0 :(得分:1)

这是一个有趣的挑战。我认为我使用表达式API为您的AutoSetup方法提供了有效的实现。如果有人有更简单的解决方案,我很乐意看到它。

static void AutoSetup<TMock, TResult>(Mock<TMock> mock, Expression<Func<TMock, TResult>> exprTree, object[] items) where TMock : class
{
    var methodCallExpr = exprTree.Body as MethodCallExpression;
    var arguments = items.Skip(1).Select(o => Expression.Constant(o));
    var updatedMethod = methodCallExpr.Update(methodCallExpr.Object, arguments);
    var updatedLambda = exprTree.Update(updatedMethod, exprTree.Parameters);

    mock.Setup(updatedLambda).Returns((TResult)items[0]);
}

以下是作为控制台应用的完整工作测试,其实现为ForEachRow,您没有提供。

class Program
{
    static void Main(string[] args)
    {
        var login = SetUp();
        Console.WriteLine(login.Login("Jack", "123!@#"));
        Console.WriteLine(login.Login("Jack", "123321"));
        Console.WriteLine(login.Login("Peter", "123!@#"));
        Console.ReadKey();
    }

    static ILogin SetUp()
    {
        var mock = new Mock<ILogin>(MockBehavior.Strict);

        var rows = new object[,]
        {
            { LoginResult.Success, "Jack", "123!@#" },
            { LoginResult.WrongPassword, "Jack", "123321" },
            { LoginResult.NoSuchUser, "Peter", "123!@#" }
        };

        rows.ForEachRow((row) => AutoSetup(mock, (ILogin l) => l.Login("ANY", "ANY"), row));

        return mock.Object;
    }

    private static void AutoSetup<TMock, TResult>(Mock<TMock> mock, Expression<Func<TMock, TResult>> exprTree, object[] items) where TMock : class
    {
        var methodCallExpr = exprTree.Body as MethodCallExpression;
        var arguments = items.Skip(1).Select(o => Expression.Constant(o));
        var updatedMethod = methodCallExpr.Update(methodCallExpr.Object, arguments);
        var updatedLambda = exprTree.Update(updatedMethod, exprTree.Parameters);

        mock.Setup(updatedLambda).Returns((TResult)items[0]);
    }
}

public static class ArrayExtensions
{
    public static void ForEachRow<T>(this T[,] rows, Action<T[]> action)
    {
        var x = rows.GetLength(1);
        var y = rows.GetLength(0);
        for (int i = 0; i < y; i++)
        {
            var row = new T[x];
            for (int j = 0; j < x; j++)
            {
                row[j] = rows[i, j];
            }
            action(row);
        }
    }
}

public interface ILogin
{
    LoginResult Login(string p1, string p2);
}

public enum LoginResult
{
    Success,
    WrongPassword,
    NoSuchUser
}

编辑:您在评论中询问了如何利用Moq为It.IsAny<>方法提供的变量参数匹配。因为传递给Mock.Setup()的是表达式树,所以它能够扫描方法参数并为任何调用It.IsAny<>的行为实现特殊行为。但是,如果在测试数据数组中使用It.IsAny<>,那么当我们从items数组中检索它以设置表达式时,它不是方法调用,而只是调用它的结果It.IsAny<> default(T) (see here)

我们需要一些在测试数据数组中指定参数应该是any的方法。然后我们可以检查这种特殊情况并生成代表MethodCallExpression调用的正确It.IsAny<>

以下是我为支持此项所做的更改:

添加Any类型以在测试数据中使用

public class Any<T>
{
    private Any() { }
    public static Any<T> Param { get { return new Any<T>(); } }
}

更新AutoSetup方法以处理这种特殊情况:

private static void AutoSetup<TMock, TResult>(Mock<TMock> mock, Expression<Func<TMock, TResult>> exprTree, object[] items) where TMock : class
{
    var arguments = items.Skip(1).Select(o => {
        var type = o.GetType();
        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Any<>))
        {
            var typeParameter = type.GetGenericArguments();
            var genericItIsAny = typeof(It).GetMethod("IsAny");
            var itIsAny = genericItIsAny.MakeGenericMethod(typeParameter);
            return Expression.Call(itIsAny) as Expression;
        }
        return Expression.Constant(o);
    });

    var methodCallExpr = exprTree.Body as MethodCallExpression;
    var updatedMethod = methodCallExpr.Update(methodCallExpr.Object, arguments);
    var updatedLambda = exprTree.Update(updatedMethod, exprTree.Parameters);

    mock.Setup(updatedLambda).Returns((TResult)items[0]);
}

在测试数据中,使用Any类型

{ LoginResult.Success, "NoPasswordUser", Any<string>.Param }

根据您存储测试数据的方式,您可能需要另一种方法来识别您想要变量的参数(特殊格式化的字符串?),但一般情况下,这应该可以让您了解如何创建正确的表达方式。