もくじ
https://tera1707.com/entry/2022/02/06/144447
やりたいこと
昔から、UnitTestせねば、自動テストせねば、そのためにDI(DependencyInjection)せねば、、、と思い続けてたのに、どんどん後回しになって全然やってなかったので、まずは簡単にDIの練習をしてみる。
前提
- Windows10 Home 21H1 19043.1706
- VisualStudio2022 Community 17.1.4
- WinUI3.0
- Windows App SDK 1.0
- NUnitで単体テスト実施
- 2022年5月の時点の調査
今回使用したnugetパッケージ
CommunityToolkit.Mvvm
→ Ioc のために使用。
Microsoft.Extensions.DependencyInjection
→ ServiceCollection のために使用。
DIコンテナにはいろいろある様子だが、今知ってるもので、一番簡単に使えそうなものを選んだ。
サンプルコード
今回は実験のため、
- 現在時刻を取ってくるModelがあるとする。
- そいつを使う親Model(ParentModel)がいて、そいつの持ってるメソッドがテスト対象とする。
- 本番用のコードではそのParentModelを画面のコードが、テスト用のコードでは、ParentModelをUnitTestのコードが呼ぶものとする。
- ParentModelは、現在時刻を返してくるGetCurrentTimeModeというクラスを本番では使うが、UnitTest時に、どんな値が来るかわからない「現在時刻」を返されてしまうと期待値が決められずテストできないので、UnitTest時は、ParentModelが呼ぶのを固定の日付を返すGetCurrentTimeModeMockというModelクラスに差し替えてテストする。
- UnitTestコードでは、その固定の日付を元に期待値を決めて、それと実際のテスト対象のParentModelのメソッドの処理の結果が一致するかを確認するコードを書く。
- 図にすると下記のようなイメージ。
本番コード
App.xaml.cs
using CommunityToolkit.Mvvm.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; namespace DependencyInjectionJikken { public partial class App : Application { private Window m_window; public App() { this.InitializeComponent(); var services = new ServiceCollection(); services.AddSingleton<IGetCurrentTimeModel, GetCurrentTimeModel>(); var provider = services.BuildServiceProvider(); Ioc.Default.ConfigureServices(provider); } protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { m_window = new MainWindow(); m_window.Activate(); } } }
MainWindow.xaml.cs
using Microsoft.UI.Xaml; namespace DependencyInjectionJikken { public sealed partial class MainWindow : Window { public MainWindow() { this.InitializeComponent(); } private void myButton_Click(object sender, RoutedEventArgs e) { var pm = new ParentModel(); myButton.Content = pm.GetCurrentTime().ToString(); } } }
GetCurrentTimeModel
using System; namespace DependencyInjectionJikken { public class GetCurrentTimeModel : IGetCurrentTimeModel { public DateTime Get() => DateTime.Now; } }
UnitTestコード
UnitTest1.cs
using CommunityToolkit.Mvvm.DependencyInjection; using DependencyInjectionJikken; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using System; namespace TestProject1 { public class Tests { [SetUp] public void Setup() { var services = new ServiceCollection(); services.AddSingleton<IGetCurrentTimeModel, GetCurrentTimeModelMock>(); var provider = services.BuildServiceProvider(); Ioc.Default.ConfigureServices(provider); } [Test] public void Test1() { var pm = new ParentModel(); var answer = pm.GetCurrentTime(); var expect = new DateTime(2000, 1, 1, 0, 0, 0); Assert.AreEqual(expect.ToString(), answer.ToString()); } } }
GetCurrentTimeModelMock
using System; namespace DependencyInjectionJikken { public class GetCurrentTimeModelMock : IGetCurrentTimeModel { public DateTime Get() => new DateTime(2000, 1, 1, 0, 0, 0); } }
本番/UnitTest共通コード
ParentModel.cs
using CommunityToolkit.Mvvm.DependencyInjection; using System; namespace DependencyInjectionJikken { public class ParentModel { IGetCurrentTimeModel gctm; public ParentModel() => gctm = Ioc.Default.GetRequiredService<IGetCurrentTimeModel>(); public DateTime GetCurrentTime() => gctm.Get(); } }
IGetCurrentTimeModel
using System; namespace DependencyInjectionJikken { public interface IGetCurrentTimeModel { DateTime Get(); } }
UnitTestについて
WinUI3のプロジェクトに対してMSTestでテストしようとすると、どうしてもテストが実行できなかった。
代わりに、今回はNUnitでテストを実施。(MSTestのバグ??)
やったことポイント
本番では、テスト対象のParentModel
はGetCurrentTimeModel
を使い、テストではGetCurrentTimeModelMock
を使う。
nugetしたMicrosoft.Extensions.DependencyInjection
がもつServiceCollection
に、本番画面コードでは本番用Modelを登録し、テストコードではテスト用ModelMockをserviceとして登録する。
本番
var services = new ServiceCollection();
services.AddSingleton<IGetCurrentTimeModel, GetCurrentTimeModel>();
var provider = services.BuildServiceProvider();
テスト
var services = new ServiceCollection();
services.AddSingleton<IGetCurrentTimeModel, GetCurrentTimeModelMock>();
var provider = services.BuildServiceProvider();
そのserviceを、nugetしたCommunityToolkit.Mvvm
が持つIoc
のDefault
というコンテナに、登録してやる。
本番/テスト両方
Ioc.Default.ConfigureServices(provider);
それを使う部分で、登録したservice(Model)を取り出してやる。
本番/テスト両方
// 取り出すときは、interfaceで取り出す。
IGetCurrentTimeModel gctm = Ioc.Default.GetRequiredService<IGetCurrentTimeModel>();
取り出し時に、本番用かMockのどちらが取り出されるかは、登録したときのModel次第なので、 本番時は本番用Modelが取り出され、テスト時はMockが取り出される、という仕組み。
その他メモ、、、
同じことをやるために、今回だとParentModelのコンストラクタに、その上の階層でnewしたMockを渡してやって、UnitTestする、とかもできるが、それだと使うModelが増えたときに使う側でParentModelのコンストラクタにたくさんModelを渡してやらないといけなくなる。それが大変なので、こういうDIコンテナを使う、という現状の理解。
(もっとほかにもいろいろ便利ポイントがありそうだが)
参考
MSのDIのやり方解説ページ
まずはここをよく読まねば、、、
https://docs.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection
Microsoft.Extensions.DependencyInjection
https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection
DIコンテナ「Microsoft.Extensions.DependencyInjection」の紹介記事。 Microsoft.Extensions.DependencyInjectionは、今回やった内容よりも高度なことができる様子。 余裕があればやってみる。
https://qiita.com/okazuki/items/239ca5ef46e5a085e085
Ioc、ServiceCollection を使ってDIをしているコードを解説する動画。わかりやすい。 (本題はテーマカラーの話だが、それのためにDIしてる)