web-dev-qa-db-ja.com

NSubstitute DbSet / IQueryable <T>

したがって、EntityFramework 6は、以前のバージョンよりもはるかにテストが容易です。そして、Moqのようなフレームワーク用のインターネット上に いくつかの素晴らしい例 がありますが、場合によっては、NSubstituteの使用を好みます。 NSubstituteを使用して動作するように翻訳された「非クエリ」の例を持っていますが、「クエリテスト」を理解することができません。

Moqのitems.As<IQueryable<T>>().Setup(m => m.Provider).Returns(data.Provider);はNSubstituteにどのように変換されますか? _((IQueryable<T>) items).Provider.Returns(data.Provider);_のようなものを考えましたが、うまくいきませんでした。私もitems.AsQueryable().Provider.Returns(data.Provider);を試しましたが、それもうまくいきませんでした。

私が得ている例外は:

"System.NotImplementedException:メンバー 'IQueryable.Provider'はタイプ 'DbSet _1Proxy' which inherits from 'DbSet_ 1'に実装されていません。'DbSet`1 'のテストdoubleは、使用されるメソッドとプロパティの実装を提供する必要があります。

上記のリンクからコード例を引用させてください。このコードサンプルでは、​​Moqを使用してDbContextとDbSetをモックします。

_public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  }.AsQueryable();

  var mockSet = new Mock<DbSet<Blog>>();
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

  var mockContext = new Mock<BloggingContext>();
  mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);

  // ...
}
_

そして、これは私がNSubstituteと一緒に来るところです

_public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  }.AsQueryable();

  var mockSet = Substitute.For<DbSet<Blog>>();
  // it's the next four lines I don't get to work
  ((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
  ((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
  ((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
  ((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());

  var mockContext = Substitute.For<BloggingContext>();
  mockContext.Blogs.Returns(mockSet);

  // ...
}
_

だから問題は(プロバイダーのような)IQueryableのプロパティをどのように置き換えますか?

37
s.meijer

これは、NSubstitute構文固有のために発生します。例:

((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);

NSubstituteはプロバイダーのゲッターを呼び出し、次に戻り値を指定します。このゲッターコールは代用者によってインターセプトされず、例外が発生します。これは、DbQueryクラスのIQueryable.Providerプロパティの明示的な実装が原因で発生します。

NSubを使用して複数のインターフェースの代替を明示的に作成でき、指定されたすべてのインターフェースをカバーするプロキシを作成します。次に、インターフェイスへの呼び出しは、代替によって傍受されます。次の構文を使用してください:

// Create a substitute for DbSet and IQueryable types:
var mockSet = Substitute.For<DbSet<Blog>, IQueryable<Blog>>();

// And then as you do:
((IQueryable<Blog>)mockSet).Provider.Returns(data.Provider);
...
39

Kevinのおかげで、コードの翻訳に問題が見つかりました。

nittest code samplesDbSetをあざけっていますが、NSubstituteにはインターフェースの実装が必要です。したがって、NSubstituteのMoqs new Mock<DbSet<Blog>>()に相当するのはSubstitute.For<IDbSet<Blog>>()です。必ずしもインターフェースを提供する必要はないので、私は混乱しました。しかし、この特定のケースでは、それが重要であることが判明しました。

また、IDbSetインターフェイスを使用する場合は、Queryableにキャストする必要がないこともわかりました。

したがって、実際のテストコード:

public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
    new Blog { Name = "BBB" },
    new Blog { Name = "ZZZ" },
    new Blog { Name = "AAA" },
  }.AsQueryable();

  var mockSet = Substitute.For<IDbSet<Blog>>();
  mockSet.Provider.Returns(data.Provider);
  mockSet.Expression.Returns(data.Expression);
  mockSet.ElementType.Returns(data.ElementType);
  mockSet.GetEnumerator().Returns(data.GetEnumerator());

  var mockContext = Substitute.For<BloggingContext>();
  mockContext.Blogs.Returns(mockSet);

  // Act and Assert ...
}

単体テストのArrangeセクションをクリーンアップする小さな拡張メソッドを作成しました。

public static class ExtentionMethods
{
    public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
    {
        dbSet.Provider.Returns(data.Provider);
        dbSet.Expression.Returns(data.Expression);
        dbSet.ElementType.Returns(data.ElementType);
        dbSet.GetEnumerator().Returns(data.GetEnumerator());
        return dbSet;
    }
}

// usage like:
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);

問題ではありませんが、非同期操作もサポートできるようにする必要がある場合:

public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
  dbSet.Provider.Returns(data.Provider);
  dbSet.Expression.Returns(data.Expression);
  dbSet.ElementType.Returns(data.ElementType);
  dbSet.GetEnumerator().Returns(data.GetEnumerator());

  if (dbSet is IDbAsyncEnumerable)
  {
    ((IDbAsyncEnumerable<T>) dbSet).GetAsyncEnumerator()
      .Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));
    dbSet.Provider.Returns(new TestDbAsyncQueryProvider<T>(data.Provider));
  }

  return dbSet;
}

// create substitution with async
var mockSet = Substitute.For<IDbSet<Blog>, IDbAsyncEnumerable<Blog>>().Initialize(data);
// create substitution without async
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
17
s.meijer

これは、偽のDbSetを生成するための静的汎用静的メソッドです。役に立つかもしれません。

 public static class CustomTestUtils
{
    public static DbSet<T> FakeDbSet<T>(List<T> data) where T : class
    {
        var _data = data.AsQueryable();
        var fakeDbSet = Substitute.For<DbSet<T>, IQueryable<T>>();
        ((IQueryable<T>)fakeDbSet).Provider.Returns(_data.Provider);
        ((IQueryable<T>)fakeDbSet).Expression.Returns(_data.Expression);
        ((IQueryable<T>)fakeDbSet).ElementType.Returns(_data.ElementType);
        ((IQueryable<T>)fakeDbSet).GetEnumerator().Returns(_data.GetEnumerator());

        fakeDbSet.AsNoTracking().Returns(fakeDbSet);

        return fakeDbSet;
    }

}
4

約1年前にラッパーを作成しましたが、そのコードは 独自のテストダブルス(EF6以降)を使用したテスト から参照しているものと同じです。このラッパーは GitHub DbContextMockForUnitTests にあります。このラッパーの目的は、DbContextDbSetsをモックしたいEFを利用する単体テストをセットアップするために必要な反復的/重複するコードの量を減らすことです。 OPにある模擬EFコードのほとんどは、2行のコード(に削減でき、DbSetプロパティの代わりに_DbContext.Set<T>_を使用している場合は1行だけです)そして、モックコードがラッパーで呼び出されます。

使用するには、フォルダーMockHelpersのファイルをコピーしてテストプロジェクトに含めます。

上記の例を使用したテストの例を次に示します。モックDbContextにモック_DbSet<T>_を設定するために必要なコードは2行だけであることに注意してください。

_public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  };

  var mockContext = Substitute.For<BloggingContext>();

  // Create and assign the substituted DbSet
  var mockSet = data.GenerateMockDbSet();
  mockContext.Blogs.Returns(mockSet);

  // act
}
_

これを_DbSet<T>_の.ToListAsync()のような非同期/待機パターンを使用するものを呼び出すテストにするのも同じくらい簡単です。

_public async Task GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  };

  var mockContext = Substitute.For<BloggingContext>();

  // Create and assign the substituted DbSet
  var mockSet = data.GenerateMockDbSetForAsync(); // only change is the ForAsync version of the method
  mockContext.Blogs.Returns(mockSet);

  // act
}
_
2
Igor

IQueryableのすべての部分をモックする必要はありません。 NSubstituteを使用してEF DbContextをモックするときは、次のようにします。

interface IContext
{
  IDbSet<Foo> Foos { get; set; }
}

var context = Substitute.For<IContext>();

context.Foos.Returns(new MockDbSet<Foo>());

リストまたは私のMockDbSet()用のIDbSetの単純な実装。

NSubstituteは仮想メソッドのみをオーバーライドするため、一般的には型ではなくインターフェイスをモックする必要があります。

0
Kevin