UnitTestでDependencyInjectionするときの、登録と取り出しの仕方

もくじ
https://tera1707.com/entry/2022/02/06/144447#UnitTest

やりたいこと

以前の記事で、ServiceCollectionIocを使って依存objectの登録と取り出しができることが分かった。

しかし、登録は、プログラムの初めの部分で登録したらよいのだろう、というのは何となくわかるのだが、取り出し(いわゆるResolve)をどこでどうやってやればよいのかがよくわからなかった。

勉強中に試しに書いたコードで、 依存objectを取り出すために、Ioc.Default.GetRequiredService<Interface>();をあちこちに書いているときに、これではグローバル変数をあちこちに書いているのと変わらないじゃないか、となってしまった。

ServiceCollectionやIocを活かすには、依存objectをどこでどう取り出せばいいのか?

前提

  • Windows10 Home 21H1 19043.1706
  • VisualStudio2022 Community 17.1.4
  • nugetした関連ライブラリ
    • CommunityToolkit.Mvvm(Iocのために使用)
    • Microsoft.Extensions.DependencyInjection(ServiceCollectionのために使用)
  • 2022年6月の時点の調査

結論

注入されるもの(=依存object)と一緒に、注入されるクラス(=テスト対象クラス)を一緒にコンテナに登録しておけば、テスト対象クラスに、コンテナが自動で依存objectを注入してくれる。これで、取り出し(Resolve)するのは、注入されるクラス(=テスト対象クラス)だけで済む。

→これで、DIコンテナを使う目的である、「クラスインスタンスの管理を一か所で行う」ということが実現できる感じ。

イメージ

サンプルコード

上のイメージの図にあるように、

  • MyClass1とMyClass2を、MyViewModelの中で使っている。
  • プログラム開始時に、MyClass1と2、MyViewModelをコンテナに登録する。
  • MyViewModelだけをResolveし、MyViewModelにMyClass1と2が注入済みであることを確認する。

ということをしている実験プログラム。

メイン関数

using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using ConsoleApp1;

var services = new ServiceCollection();
// 依存objectを登録
services.AddSingleton<Interface1, MyClass1>();
services.AddSingleton<Interface2, MyClass2>();
// 依存objectを使うクラスを登録
services.AddTransient<MyViewModel>();
var provider = services.BuildServiceProvider();
Ioc.Default.ConfigureServices(provider);

// 使う
var vm = Ioc.Default.GetRequiredService<MyViewModel>(); 

Console.WriteLine(vm.MyFunc12());    // 結果:33
Console.ReadLine();

services.AddTransient<MyViewModel>();の部分は、インターフェースではなくクラス名を指定して登録している。
MyClass1と2と同じように、MyViewModel用のインターフェースを作ってそれを使って登録してもいいが、ViewModelのインターフェースを作るのは面倒なので、クラス指定にした。(ただし、モックとの切り替えとかはできない。)

※モックとの切り替えはできないが、下のほうに書いたように、UnitTestではコンテナ使わないので上記でOK。

インターフェース1

namespace ConsoleApp1
{
    public interface Interface1
    {
        public int MyFunc1();
    }
}

インターフェース1を実装したクラス

namespace ConsoleApp1
{
    internal class MyClass1 : Interface1
    {
        public int MyFunc1() => 11;
    }
}

インターフェース2

namespace ConsoleApp1
{
    public interface Interface2
    {
        public int MyFunc2();
    }
}

インターフェース2を実装したクラス

namespace ConsoleApp1
{
    internal class MyClass2 : Interface2
    {
        public int MyFunc2() => 22;
    }
}

インターフェース1と2を使うクラス(ViewModel)

namespace ConsoleApp1
{
    internal class MyViewModel
    {
        Interface1 cls1;
        Interface2 cls2;

        public MyViewModel(Interface1 cls1, Interface2 cls2)
        {
            this.cls1 = cls1;
            this.cls2 = cls2;
        }

        public int MyFunc12()
        {
            return cls1.MyFunc1() + cls2.MyFunc2();
        }
    }
}

UnitTest側のコードはどうするか?

以上で、(テストコードではない)通常時のコードは、DIで実現できた。

UnitTest側も、

// 登録
var services = new ServiceCollection();
services.AddSingleton<IXxxxxx, IXxxxxxMock>();
var provider = services.BuildServiceProvider();
Ioc.Default.ConfigureServices(provider);

上記コードの、依存objectの登録を、UT用のMockのobjectに変えて、 テストのたびに登録をし直して、

Ioc.Default.GetRequiredService<IXxxxxx>();

で呼び出して使うのかと思ったが、どうやらそうではなさそう。

Ioc.Default.ConfigureServices(provider); を、一つのプロセス(UT)で何度も行うと、2回目に「System.InvalidOperationException: 'The default service provider has already been configured'」の例外が起きてしまう。

Iocは、何度も繰り返し登録を行うようにはできていないっぽい。

そのため、UnitTestを行う際は、
テスト対象のコンストラクタを自分でnewし、newするときのコンストラクタの引数に、直接テスト用のMockオブジェクトを渡すようにする。

下記のようなイメージ。

var vmTest = new MyViewModel(new MyClass1Mock, new MyClass2Mock);
// →MyClass1MockとMyClass2Mockは、Interface1とInterface2を実装した、テスト向けの動きをするように作ったMockのインスタンス。

UnitTestにもIoc使うのかと思っていたが、そうではなさそう。
まあ、それで特に不便があるわけでないので、そんなもんなんだろう、と無理やり納得した。

参考

MS公式 Ioc (Inversion of control) の説明

https://docs.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/ioc

古いIocの仕組みから、新しい(community toolkitを使った)やり方に移行するときの説明
以降の説明はおいといて、こういうことがServiceCollectionで出来るんだ、というのが見えた資料。

https://docs.microsoft.com/ja-jp/dotnet/communitytoolkit/mvvm/migratingfrommvvmlight

自分のUnitTest関連記事

https://tera1707.com/entry/2022/05/16/230008
https://tera1707.com/entry/2022/06/17/124626