Метод 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 все же; Я зашел так далеко вниз по кроличьей норе. : -)