概要
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はstaticメソッドのモック化に非対応とのことなので、IServiceProvider.GetRequiredService