Как я могу написать универсальный метод, который устанавливает ожидание объекта Moq? - PullRequest
1 голос
/ 15 марта 2012

Метод M принимает 2 параметра, P1 и P2. P2 является делегатом. Я хочу сказать фиктивному объекту: «Когда метод M вызывается с параметром P1, вызывайте P2 и передавайте ему объект O». Я использую Moq.

Следующий подход работает, но кажется немного многословным.

this.DataCacheMock = Mock.Of<IDataCache>();
var dataObject = new DataObject();

Mock.Get(this.DataCacheMock)
.Setup(m => m.GetDataObject(123, It.IsAny<EventHandler<DataPortalResult<DataObject>>>()))
.Callback((int id, EventHandler<DataPortalResult<DataObject>> callback) => callback(null, new DataPortalResult(dataObject, null, null)));

Я хотел бы преобразовать этот последний бит в универсальный вспомогательный метод, чтобы мне (и будущим авторам тестов) нужно было написать только что-то вроде этого:

TestTools.ArrangeDataPortalResult(this.DataCacheMock.GetDataObject, 123, dataObject);

Большой вопрос: что будет внутри этого вспомогательного метода? До сих пор у меня был частичный успех, но мне интересно, есть ли какой-нибудь способ пройти весь путь туда.

Первая попытка (не работает)

public static void ArrangeDataPortalResult<TMock, TResult, TParam>(
        TMock mockObject,
        Action<TMock, TParam, EventHandler<DataPortalResult<TResult>>> action,
        TParam parameter,
        TResult result)
    where TMock : class
{
    Moq.Mock.Get(mockObject)
        .Setup(m => action(m, parameter, Moq.It.IsAny<EventHandler<DataPortalResult<TResult>>>()))
        .Callback<TParam, EventHandler<DataPortalResult<TResult>>>((p, callback) =>
                callback(null, new DataPortalResult<TResult>(result, null, null)));
}

Я могу назвать этот метод так:

TestTools.ArrangeDataPortalResult<IDataCache, DataObject, int>(
    this.DataCacheMock,
    (mock, param, handler) => mock.GetDataObject(param, handler),
    dataObjectId,
    dataObject);

Как оказалось, Moq не нравится то, что я передаю в метод установки. Выдает исключение, говорящее «Выражение не является вызовом метода».

Вторая попытка

В этом подходе я выполняю некоторые манипуляции с выражениями LINQ (чего я никогда раньше не делал).

public static void ArrangeDataPortalResult<TMock, TParam, TResult>(
        TMock mockObject,
        Expression<Action<TMock>> methodCall, TResult result)
    where TMock : class
{
    // Get the method that will be called on the mock object, and the method's parameters.
    var methodCallExpression = methodCall.Body as MethodCallExpression;
    var parameters = methodCallExpression.Arguments;

    // Create a new parameter list, and substitute Moq.It.IsAny<EventHandler<DataPortalResult<TResult>>>() for the callback.
    // This is so that the test author doesn't need to write It.IsAny<blah>.
    var newParameters = parameters.Select(p => p).ToList();
    newParameters.RemoveAt(newParameters.Count - 1);
    var isAny = typeof(Moq.It).GetMethod("IsAny").MakeGenericMethod(typeof(EventHandler<DataPortalResult<TResult>>));
    var newCallbackParameterExpression = Expression.Call(null, isAny);
    newParameters.Add(newCallbackParameterExpression);

    // Create a new expression that contains the new IsAny parameter.
    var newMethodCallExpression = Expression.Call(methodCallExpression.Object, methodCallExpression.Method, newParameters);

    // Set up the mock object to expect a method call with the same parameters passed to it, but allow any callback to be passed to it.
    // Additionally, tell the mock object to immediately invoke its callback, and pass the given result to it.
    Moq.Mock.Get(mockObject)
        .Setup(Expression.Lambda<Action<TMock>>(newMethodCallExpression, methodCall.Parameters))
        .Callback<TParam, EventHandler<DataPortalResult<TResult>>>((p, callback) => callback(null, new DataPortalResult<TResult>(result, null, null)));
}

Этот метод можно назвать так.

TestTools.ArrangeDataPortalResult<IDataCache, int, DataObject>(
    this.DataCacheMock,
    mock => mock.GetDataObject(123, null),
    dataObject);

Это работает, и я мог бы согласиться на что-то подобное в случае необходимости. К сожалению, если бы я случайно вызвал неправильный метод DataCacheMock (возможно, он имеет перегрузку, которая принимает строку вместо int), я бы получил ошибку времени выполнения, а не ошибку времени компиляции.

Третья попытка

public static void ArrangeDataPortalResultMoq<TMock, TParam, TResult>(
        Expression<Action> methodCall, TResult result)
    where TMock : class
{
    // Get the method that will be called on the mock object, and the method's parameters.
    // (This part is the same.)

    // Create a new parameter list, and substitute Moq.It.IsAny<EventHandler<DataPortalResult<TResult>>>() for the callback.
    // (This part is the same.)

    // Create a new expression that contains the new IsAny parameter.
    var newMethodCallExpression = Expression.Call(Expression.Parameter(typeof(TMock), "mock"), methodCallExpression.Method, newParameters);

    // Get the real mock object referred to in the method call.
    var mockObject = Expression.Lambda<Func<TMock>>(methodCallExpression.Object).Compile()();

    // Set up the mock object to expect a method call with the same parameters passed to it, but allow any callback to be passed to it.
    // Additionally, tell the mock object to immediately invoke its callback, and pass the given result to it.
    Moq.Mock.Get(mockObject)
        .Setup(Expression.Lambda<Action<TMock>>(newMethodCallExpression, Expression.Parameter(typeof(TMock), "mock")))
        .Callback<TParam, EventHandler<DataPortalResult<TResult>>>((p, callback) => callback(null, new DataPortalResult<TResult>(result, null, null)));
}

Эта версия получает фиктивный объект из выражения, которое вы передаете ему, поэтому вам не нужно дважды упоминать фиктивный объект при вызове вспомогательного метода:

TestTools.ArrangeDataPortalResultMoq<IDataCache, int, ceQryUomsBO>(
    () => this.DataCacheMock.GetDataObject(dataObjectId, null),
    dataObject);

Этот подход все еще имеет ту же проблему с типами.

Я (и будущие авторы тестов), вероятно, мог бы иметь дело с подробным синтаксисом, упомянутым вверху, и мы могли бы, вероятно, иметь дело с меньшей безопасностью типов, поскольку тест просто не удался. Я все еще хотел бы видеть, возможно ли это с Moq все же; Я зашел так далеко вниз по кроличьей норе. : -)

1 Ответ

0 голосов
/ 04 апреля 2012

В случае, если это поможет кому-то еще, я получил решение, которое позволяет мне сделать это:

TestTools.ArrangeDataPortalResult(
    this.DataCacheMock,
    (param1, callback) => this.DataCacheMock.Object.GetDataObject(param1, callback),
    123,
    dataObject);

Это было достаточно для меня.Мое решение ниже.

public static void ArrangeDataPortalResult<TMocked, TResult, TParam>(Mock<TMocked> mock, Expression<Action<TParam, EventHandler<DataPortalResult<TResult>>>> expectedMethodCall, TParam parameter, TResult result)
    where TMocked : class
{
    var methodCallExpr = expectedMethodCall.Body as MethodCallExpression;
    var newMethodCallExpr = TransformAsyncCallForMoq<TMocked, TResult>(methodCallExpr, parameter);
    mock.Setup(newMethodCallExpr)
        .Callback<TParam, EventHandler<DataPortalResult<TResult>>>((p, callback) => callback(null, new DataPortalResult<TResult>(result, null, null)));
}

private static Expression<Action<TMocked>> TransformAsyncCallForMoq<TMocked, TResult>(MethodCallExpression methodCallExpr, params object[] expectedParameterValues)
{
    var methodCallParameters = methodCallExpr.Arguments;

    /// Transform a method call on a specific object,
    /// e.g. (param1, param2, callback) => MyMockObject.GetData(param1, param2, callback),
    /// into a lambda expression that Moq's Setup method can use, which looks more like this:
    /// m => m.GetData(5, "asdf", /* any event handler */).
    MethodCallExpression newMethodCallExpression = Expression.Call(
        Expression.Parameter(typeof(TMocked), "m"),
        methodCallExpr.Method,
        CreateParameterExpressionsWithAnyCallback(methodCallParameters, expectedParameterValues));

    return Expression.Lambda<Action<TMocked>>(newMethodCallExpression, Expression.Parameter(typeof(TMocked), "m"));
}


private static IEnumerable<Expression> CreateParameterExpressionsWithAnyCallback(IEnumerable<Expression> oldParameterExpressions, IEnumerable<object> expectedParameterValues)
{
    // Given a set of expressions and expected values, returns a new set of expressions that will 
    // allow Moq to set the proper method call expectation. Assumes there will be one more parameter
    // expression (the callback parameter) that has no expected value, and allows any value for it.

    var newParameterExpressions = oldParameterExpressions.Zip(expectedParameterValues,
        (paramExpr, paramVal) => Expression.Constant(paramVal, paramExpr.Type) as Expression);

    foreach (var expr in newParameterExpressions)
    {
        yield return expr;
    }

    var callbackParamExpr = oldParameterExpressions.Last();
    var isAny = typeof(Moq.It).GetMethod("IsAny").MakeGenericMethod(callbackParamExpr.Type);
    yield return Expression.Call(null, isAny) as Expression;
}

Если кто-нибудь знает более простой способ сделать это, я надеюсь, что вы поделитесь.: -)

...