Quantcast
Channel: プログラミング
Viewing all articles
Browse latest Browse all 7878

C#のMoqで拡張メソッドをモック化して、IServiceProviderのモックを作る方法 - 新しもの好きプログラマの耳より情報ブログ

$
0
0

概要

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を書く上ではとても便利なので、このような点で挫折せずに上手く使っていきたいところですね。


Viewing all articles
Browse latest Browse all 7878

Trending Articles