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

# 概要

NUnitなどでUnitTestを書く場合に、インターフェース部分にモックを与えるために[Moq](https://github.com/devlooped/moq)を使っている人は多いと思います。これの意外な弱点として、拡張メソッドをモック化することができません。すると、GenericHost（DI）の使い方次第では頻出となるIServiceProvider.GetRequiredService<T>()などをモックにできず、テスト困難になる場合があります。一工夫してこれをモック化する方法を紹介します。

# 最初に結論まとめ

次のようなアダプタクラスを自作し、これを間に挟む（テスト対象クラスへDIで渡す）事で、実現できます。

```csharp
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のメソッドでインスタンスを生成することがあると思います。

```csharp
IServiceProvider serviceProvider;//このインスタンスはDIで受け取る
var myInterface = serviceProvider.GetRequiredService<IMyInterface>();
```

UTコードを書く時にここの動きを変えたい場合、いつものようにMoqでメソッドの動作を設定します。

```csharp
Mock<IServiceProvider> MockIServiceProvider.Setup(d => d.GetRequiredService<IMyInterface>());
```

しかし、この書き方では動きません。GetRequiredServiceは拡張メソッドであり、IServiceProviderのメソッドではないからです。IServiceProviderが持っているのはGetServiceというメソッドです。Moqにはstaticメソッドを書き換える機能が無いので、拡張メソッドも書き換えできないようです。つまり、次の図のような状態です。

```plantuml
@startuml

component "テスト対象（本体）コード" as main {
	component "IServiceProvider.GetRequiredService()（呼び出し）" as mainmet
}
component "本物の拡張メソッド" as ada {
	component "GetRequiredService()（拡張メソッド）" as adamet
}
component "Moqのインスタンス" as moq{
	component "GetService()（モック）" as moqmet
}

mainmet --> adamet
mainmet ..> moqmet : 呼ばれない
@enduml
```

これは何とかしないと、UnitTestを書く時に困ります。

## 解決方法

呼び出したい拡張メソッドと同名のメソッドを実装した、アダプタのようなクラスを自作して、それをMoqとの間に挟めば解決できます。つまり、次の図のような使い方です。

```plantuml
@startuml

component "テスト対象（本体）コード" as main {
	component "IServiceProvider.GetRequiredService()（呼び出し）" as mainmet
}
component "アダプタクラス（自作）" as ada {
	component "GetRequiredService()（メソッド）" as adamet
}
component "Moqのインスタンス" as moq{
	component "GetService()（モック）" as moqmet
}

mainmet --> adamet
adamet --> moqmet
@enduml
```

IServiceProviderの場合、拡張メソッドGetRequiredServiceの呼び出しに対応してIServiceProviderが持っているメソッドがGetServiceなので、アダプタクラスは次のコードのようになります。

```csharp
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());
    }
}

```

これをテストコード側でどう使うかというと、次のようになります。

```csharp
//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<T>()を呼び出した時に、アダプタクラスを介して、Moqのインスタンス（mockIServiceProvider）が呼び出されるようになります。つまり、IServiceProvider.GetRequiredService<T>()の呼び出しをモックにすることができました。

# まとめ

Moqはstaticメソッドのモック化に非対応とのことなので、IServiceProvider.GetRequiredService<T>()のようなケースには対応できない・・・かと思いましたが、ちょっとした工夫で何とかなりました。MoqはUnitTestを書く上ではとても便利なので、このような点で挫折せずに上手く使っていきたいところですね。