Purpose
UnitTestでDB Contextを使ったServiceをテストする方法について調べる。
直書き
Moqを使ってEntityframeworkのモックを作成するには?【単体テスト】を参考に、テストを作成した。
public void ConfirmOperationForDbContext_OneItemIsActive_ReturnFoundItem() { #region Arange //DBから取得したデータを想定したテストデータを作成する。 var oneItem = new AspNetUsers() { Id = "SampleId", UserName = "001" }; var resultList = (new List<AspNetUsers>() { oneItem, new AspNetUsers() }).AsQueryable(); //DBSetのモックを作成し、先ほど作成したテストデータとその他もろもろを紐づける。 var contextStub = new Mock<DbSet<AspNetUsers>>(); contextStub.As<IQueryable<AspNetUsers>>().Setup(m => m.Provider).Returns(resultList.Provider); contextStub.As<IQueryable<AspNetUsers>>().Setup(m => m.Expression).Returns(resultList.Expression); contextStub.As<IQueryable<AspNetUsers>>().Setup(m => m.ElementType).Returns(resultList.ElementType); contextStub.As<IQueryable<AspNetUsers>>().Setup(m => m.GetEnumerator()).Returns(resultList.GetEnumerator()); //DB Contextのモックと紐づける。 var mockContext = new Mock<sampleContext>(); mockContext.Setup(m => m.AspNetUsers).Returns(contextStub.Object); #endregion #region Action //テスト対象のサービスのインスタンスを作成し、テストを行う。 var targetService = new ManageUserService(mockContext.Object); var actualUser = targetService.GetUserId(); #endregion #region Assert //結果比較 Assert.Equal(oneItem.Id, actualUser); #endregion }
とりあえずできはしたが、Contextのモックを作成するために毎回このコードを実装するのはつらい。また、メンテナンスも大変になる。そこで、DBContextのモックをシンプルに作成するためのライブラリを用意することにした。
ライブラリ化
最終的にCLUD操作に対応できるようなライブラリにしたい。が、まずは、上記のコードをRのところだけをカバーするものを作成した。
いきなりライブラリするのもハードルが高いため、Privateメソッドとして定義した。そのメソッドを使って修正したバージョンのテストコードは以下の通り。
[Fact] public void ConfirmOperationForDbContext_OneItemIsActive_ReturnFoundItem2() { #region Arange var oneItem = new AspNetUsers() { Id = "SampleId", UserName = "001" }; var resultList = (new List<AspNetUsers>() { oneItem, new AspNetUsers() }).AsQueryable(); var contextStub = this.CreateDbContextStub<sampleContext, AspNetUsers>(resultList, (i) => i.AspNetUsers); #endregion #region Action var targetService = new ManageUserService(contextStub.Object); var actualUser = targetService.GetUserId(); #endregion #region Assert Assert.Equal(oneItem.Id, actualUser); #endregion }
Privateメソッドの中身は以下の通り。
private Mock<T> CreateDbContextStub<T, S>(IQueryable<S> stubData, Expression<Func<T, DbSet<S>>> expression) where T : DbContext where S : class { var contextStub = new Mock<DbSet<S>>(); contextStub.As<IQueryable<S>>().Setup(m => m.Provider).Returns(stubData.Provider); contextStub.As<IQueryable<S>>().Setup(m => m.Expression).Returns(stubData.Expression); contextStub.As<IQueryable<S>>().Setup(m => m.ElementType).Returns(stubData.ElementType); contextStub.As<IQueryable<S>>().Setup(m => m.GetEnumerator()).Returns(stubData.GetEnumerator()); var mockContext = new Mock<T>(); mockContext.Setup<DbSet<S>>(expression).Returns(contextStub.Object); return mockContext; }
なかなか微妙な出来栄え。 mockContext.Setupで、Contextの中のどのクラスを利用するかを設定するのが難しかったため、メソッド引数でFunction自体を引き渡すように設定した。
お手本(entity-framework-core-mock)
独自ライブラリを拡張していき、可読性・保守性が高いテストを作ろうとした矢先、entity-framework-core-mockなるものを見つけた。これは、Entity Frameworkのテストを容易にするためのライブラリで、MoqとNSubstituteに対応している。すげぇ。
設定
- entity-framework-core-mockをNugetでインストール。
- Usingを設定。
- 以上。
コード
まず、DB ContextのMockインスタンスを作成する。var dbContextMock = new DbContextMock<sampleContext>();
さらに、このContextのメソッドのCreateDBSetMockメソッドを呼び出し、インスタンス内部でDbSet情報を設定する。
ここで、メソッドの引数に、x=>x.AspNetUsers
Functionを渡すことで、目的のEntityへの操作を指定することができる。
全体像は以下の通り。
public void ConfirmOperationForDbContext_OneItemIsActive_ReturnFoundItem3() { #region Arange var oneItem = new AspNetUsers() { Id = "SampleId", UserName = "001" }; var resultList = (new List<AspNetUsers>() { oneItem, new AspNetUsers() }).AsQueryable(); var dbContextMock = new DbContextMock<sampleContext>(); dbContextMock.CreateDbSetMock(x => x.AspNetUsers, resultList); #endregion #region Action var targetService = new ManageUserService(dbContextMock.Object); var actualUser = targetService.GetUserId(); #endregion #region Assert Assert.Equal(oneItem.Id, actualUser); #endregion }
ref
まとめ
DB ContextのMockを作成するためには、DbSetのモックを作成し、そのMockにいろいろな設定を追加する必要がある。その際、利用したい対象のEntity(テーブル)についての設定をする必要がある。これらの設定を行うために必要なコードは6行程度ではあるが、テストケースごとに実装するのは避けたい。そんな時は、共有して利用できるメソッドを利用するか、entity-framework-core-mockのような素敵なライブラリを利用することで対応することができる。正直entity-framework-core-mockにはどんな機能があるのか十分に理解できていない。とはいえ、ある程度の規模があるライブラリであるため、ほんとに最小限の機能だけで進めたい場合は、共通メソッドを用意して使いまわすのがよさそう。
ということで、引き続き今回作成したテストメソッドを改善していこうと思う。