メソッド M は、P1 と P2 の 2 つのパラメーターを取ります。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 は私が Setup メソッドに渡すものを気に入らないのです。「式はメソッド呼び出しではありません」という例外をスローします。
2 回目の試行
このアプローチでは、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 ではなく文字列を取るオーバーロードが含まれている可能性があります)、コンパイル時エラーではなく実行時エラーが発生します。
3 回目の試行
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)));
}
このバージョンは、渡された式からモック オブジェクトを取得するため、ヘルパー メソッドを呼び出すときにモック オブジェクトを 2 回指定する必要はありません。
TestTools.ArrangeDataPortalResultMoq<IDataCache, int, ceQryUomsBO>(
() => this.DataCacheMock.GetDataObject(dataObjectId, null),
dataObject);
ただし、このアプローチには、型に関しても同じ問題があります。
私 (および将来のテスト作成者) はおそらく、冒頭で述べた冗長な構文に対処できるでしょう。また、テストが失敗するだけなので、型の安全性が低いものにも対処できるでしょう。ただし、これが Moq で可能かどうかを確認したいと思います。私はここまでウサギの穴に落ちました。:-)