如何编写一个通用方法来设置Moq对象期望值
本文关键字:方法 设置 Moq 期望值 对象 何编写 一个 | 更新日期: 2023-09-27 18:27:51
方法M采用两个参数,P1和P2。P2是代表。我想告诉一个mock对象,"每当用参数P1调用方法M时,调用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);
最大的问题是:helper方法内部会包含什么?到目前为止,我已经取得了部分成功,但我想知道是否有办法一直做到这一点。
第一次尝试(无效)
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不喜欢我传递给Setup方法的内容。它抛出一个异常,表示"表达式不是方法调用"。
第二次尝试
在这种方法中,我对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)));
}
这个版本从传递给它的表达式中获取mock对象,所以在调用helper方法时不必两次提到mock对象:
TestTools.ArrangeDataPortalResultMoq<IDataCache, int, ceQryUomsBO>(
() => this.DataCacheMock.GetDataObject(dataObjectId, null),
dataObject);
不过,这种方法在类型方面仍然存在相同的问题。
我(以及未来的测试作者)可能会处理顶部提到的冗长语法,我们可能会处理较低的类型安全性,因为测试会失败。不过,我还是想看看Moq是否有可能做到这一点;我已经掉进兔子洞这么远了
如果它能帮助其他人,我最终找到了一个允许我这样做的解决方案:
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;
}
如果有人知道一个更简单的方法,我希望你能分享。:-)