簡體   English   中英

NSubstitute DbSet / IQueryable<t></t>

[英]NSubstitute DbSet / IQueryable<T>

所以 EntityFramework 6 比以前的版本更容易測試。 互聯網上有一些不錯的框架示例,如 Moq,但實際情況是,我更喜歡使用 NSubstitute。 我已經將“非查詢”示例翻譯為使用 NSubstitute,但我無法理解“查詢測試”。

起訂量如何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’尚未在1Proxy' which inherits from 'DbSet上實現。‘DbSet`1’的測試替身必須提供所使用的方法和屬性的實現。”

因此,讓我引用上面鏈接中的代碼示例。 此代碼示例使用 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 的屬性(如 Provider)?

發生這種情況是因為 NSubstitute 語法特定。 例如在:

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

NSubstitute 調用 Provider 的 getter,然后指定返回值。 這個 getter 調用不會被替代者攔截,你會得到一個異常。 這是因為在 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);
((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());

感謝 Kevin,我在代碼翻譯中發現了問題。

unittest 代碼示例DbSet ,但 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 ...
}

我編寫了一個小的擴展方法來清理單元測試的排列部分。

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);

這是我生成假 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;
    }

}

大約一年前,我編寫了一個包裝器,其中包含您Testing with Your Own Test Doubles (EF6 onwards)中引用的相同代碼。 這個包裝器可以在GitHub DbContextMockForUnitTests找到 這個包裝器的目的是減少設置單元測試所需的重復/重復代碼的數量,這些單元測試使用 EF 來模擬DbContextDbSets 您在 OP 中擁有的大多數模擬 EF 代碼可以減少到 2 行代碼(如果您使用DbContext.Set<T>而不是 DbSet 屬性,則只有 1行),然后在包裝器中調用模擬代碼。

要使用它, MockHelpers文件夾中的文件復制並包含到您的測試項目中。

以下是使用上面的你的曾經,通知,現在只有2行代碼需要設置模擬的示例測試DbSet<T>在嘲笑DbContext

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
}

使這個測試調用使用 async/await 模式的.ToListAsync()DbSet<T>上的DbSet<T> .ToListAsync()同樣.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
}

您不需要模擬 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 只會覆蓋虛擬方法。

當你使用類似的東西時

MyDbContext.CounterpartyDbSet.AsQuariable()    // or AsNoTracking()
    .bla().bla().bla()

你可以通過簡單的方式走路:

var counterpartyList = new List<Counterparty>()
{ 
    // some items here;
}

var myDbContext = Substitute.For<IMyDbContext>();
var counterpartySet = Substitute.For<DbSet<Counterparty>>();
counterpartySet.AsQueryable()    // or AsNoTracking()
    .Returns(counterpartyList.AsQueryable());
myDbContext.CounterpartyDbSet.Returns(counterpartySet);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM