0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#のMoqで拡張メソッドをモック化して、IServiceProviderのモックを作る方法

Last updated at Posted at 2024-10-02

概要

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

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?