もくじ
https://tera1707.com/entry/2022/02/06/144447#UnitTest
やりたいこと
以前の記事で、ServiceCollection
とIoc
を使って依存objectの登録と取り出しができることが分かった。
しかし、登録は、プログラムの初めの部分で登録したらよいのだろう、というのは何となくわかるのだが、取り出し(いわゆるResolve)をどこでどうやってやればよいのかがよくわからなかった。
勉強中に試しに書いたコードで、
依存objectを取り出すために、Ioc.Default.GetRequiredService<Interface>();
をあちこちに書いているときに、これではグローバル変数をあちこちに書いているのと変わらないじゃないか、となってしまった。
ServiceCollectionやIocを活かすには、依存objectをどこでどう取り出せばいいのか?
前提
- Windows10 Home 21H1 19043.1706
- VisualStudio2022 Community 17.1.4
- nugetした関連ライブラリ
- 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