概要
NUnitなどでUnitTestを書く場合に、インターフェース部分にモックを与えるためにMoqを使っている人は多いと思います。これの意外な弱点として、拡張メソッドをモック化することができません。すると、GenericHost(DI)の使い方次第では頻出となるIServiceProvider.GetRequiredService()などをモックにできず、テスト困難になる場合があります。一工夫してこれをモック化する方法を紹介します。
最初に結論まとめ
次のようなアダプタクラスを自作し、これを間に挟む(テスト対象クラスへDIで渡す)事で、実現できます。
internal class IServiceProviderMock(Mock<IServiceProvider> m_Moq) : IServiceProvider
{
public object? GetService(Type serviceType)
{
return m_Moq.Object.GetService(serviceType);
}
public T GetRequiredService<T>() where T : notnull
{
return (T)(m_Moq.Object.GetService(typeof(T)) ?? throw new NullReferenceException());
}
}
説明
困るケース
Microsoft のDIを使用していると、次のようにIServiceProviderのメソッドでインスタンスを生成することがあると思います。
IServiceProvider serviceProvider;//このインスタンスはDIで受け取る
var myInterface = serviceProvider.GetRequiredService<IMyInterface>();
UTコードを書く時にここの動きを変えたい場合、いつものようにMoqでメソッドの動作を設定します。
Mock<IServiceProvider> MockIServiceProvider.Setup(d => d.GetRequiredService<IMyInterface>());
しかし、この書き方では動きません。GetRequiredServiceは拡張メソッドであり、IServiceProviderのメソッドではないからです。IServiceProviderが持っているのはGetServiceというメソッドです。Moqにはstaticメソッドを書き換える機能が無いので、拡張メソッドも書き換えできないようです。つまり、次の図のような状態です。
これは何とかしないと、UnitTestを書く時に困ります。
解決方法
呼び出したい拡張メソッドと同名のメソッドを実装した、アダプタのようなクラスを自作して、それをMoqとの間に挟めば解決できます。つまり、次の図のような使い方です。
IServiceProviderの場合、拡張メソッドGetRequiredServiceの呼び出しに対応してIServiceProviderが持っているメソッドがGetServiceなので、アダプタクラスは次のコードのようになります。
internal class IServiceProviderMock(Mock<IServiceProvider> m_Moq) : IServiceProvider
{
public object? GetService(Type serviceType)
{
return m_Moq.Object.GetService(serviceType);
}
public T GetRequiredService<T>() where T : notnull
{
return (T)(m_Moq.Object.GetService(typeof(T)) ?? throw new NullReferenceException());
}
}
これをテストコード側でどう使うかというと、次のようになります。
//Moqのインスタンスを通常通りに作成
Mock<IServiceProvider> mockIServiceProvider = new();
//アダプタクラスのインスタンスを作り、Moqのインスタンスを渡す
IServiceProviderMock mockIServiceProviderMock = new(mockIServiceProvider);
//テスト対象のインスタンスへのDIには、アダプタクラスのインスタンスの方を渡す
var testTarget = new TestTarget(mockIServiceProviderMock);
//SetupなどMoqのインスタンスの設定は、通常通りに行う
//IMyInterfaceは一例であり、GetRequiredService<T>のTに渡す型を指定する
mockIServiceProvider.Setup(d => d.GetService(typeof(IMyInterface)));
//テスト対象のメソッドを呼び出す
testTarget.TestMethod();
このようにすることで、テスト対象のインスタンスがIServiceProvider.GetRequiredService()を呼び出した時に、アダプタクラスを介して、Moqのインスタンス(mockIServiceProvider)が呼び出されるようになります。つまり、IServiceProvider.GetRequiredService()の呼び出しをモックにすることができました。
まとめ
Moqはstaticメソッドのモック化に非対応とのことなので、IServiceProvider.GetRequiredService()のようなケースには対応できない・・・かと思いましたが、ちょっとした工夫で何とかなりました。MoqはUnitTestを書く上ではとても便利なので、このような点で挫折せずに上手く使っていきたいところですね。