NSubstitute DbSet / IQueryable<T>

Posted

技术标签:

【中文标题】NSubstitute DbSet / IQueryable<T>【英文标题】: 【发布时间】:2014-01-30 22:35:36 【问题描述】:

因此,EntityFramework 6 的可测试性比以前的版本要好得多。互联网上有 some nice examples 用于 Moq 等框架,但情况是,我更喜欢使用 NSubstitute。我已经翻译了“非查询”示例以使用 NSubstitute,但我无法理解“查询测试”。

Moq 的 items.As&lt;IQueryable&lt;T&gt;&gt;().Setup(m =&gt; m.Provider).Returns(data.Provider); 如何转换为 NSubstitute?我想像((IQueryable&lt;T&gt;) items).Provider.Returns(data.Provider); 这样的东西,但没用。我也试过items.AsQueryable().Provider.Returns(data.Provider);,但也没有用。

我得到的例外是:

"System.NotImplementedException : 成员 'IQueryable.Provider' 尚未在类型“DbSet1Proxy' which inherits from 'DbSet1”上实现。 '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)?

【问题讨论】:

更新:使用提供 DbAsyncQueryProvider 实现的 EntityFramework.Testing.NSubstitute 包。 【参考方案1】:

发生这种情况是因为 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());

【讨论】:

谢谢!这是非常有用的信息。我还不确定这是否是替代 DbSet 的最佳方法(事实证明,使用 IDbSet 可以解决问题),但它在其他情况下肯定会有所帮助。 我将您标记为答案,因为您的建议是支持异步的解决方案(Substitute.For, IDbAsyncEnumerable>())。这不是问题的真正问题,而是在于它的扩展。再次感谢您的洞察力! 提示:确保您的 DbSets 在您的上下文中是 virtual 有处理.Include()问题的优雅解决方案吗?我不断收到source is null 错误。【参考方案2】:

感谢 Kevin,我在我的代码翻译中发现了问题。

unittest code samples 正在模拟 DbSet,但 NSubstitute 需要接口实现。因此,NSubstitute 的起订量 new Mock&lt;DbSet&lt;Blog&gt;&gt;() 的等价物是 Substitute.For&lt;IDbSet&lt;Blog&gt;&gt;()。您并不总是需要提供接口,所以这就是我感到困惑的原因。但在这个特定的案例中,事实证明它是至关重要的。

事实证明,我们在使用接口 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);

【讨论】:

没关系。但是TestDbAsyncEnumerator和TestDbAsyncQueryProvider是什么。 这是一个内存数据库查询提供程序,正如我在我的问题中提供的链接以及这个答案所解释的那样。看看msdn页面:msdn.microsoft.com/nl-nl/data/dn314429.aspx#async 我已经尝试过了,但是得到了一个异常:NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException: Can not return value of type IDbSet1Proxy for BloggingContext.get_Blogs (expected type DbSet1)。上下文由 EF TT 模板生成 @Shevek 有同样的问题,你可以通过调整for调用中的接口/类型使其工作:Substitute.For&lt;IDbSet&lt;DataModel&gt;, DbSet&lt;DataModel&gt;&gt;(); @Shevek 你也可以修改你的 BloggingContext 类:public virtual IDbSet&lt;Blog&gt; Blogs get; set; public virtual IDbSet&lt;Post&gt; Posts get; set; 【参考方案3】:

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


【讨论】:

我认为参数“data”的定义中缺少关键字“this” 在几个项目中使用了这个。我喜欢它从我的测试中移除管道【参考方案4】:

大约一年前,我围绕您从Testing with Your Own Test Doubles (EF6 onwards) 引用的相同代码编写了一个包装器。这个包装可以在GitHub DbContextMockForUnitTests 上找到。此包装器的目的是减少设置单元测试所需的重复/重复代码的数量,这些单元测试使用您要模拟 DbContextDbSets 的 EF。您在 OP 中拥有的大多数模拟 EF 代码可以减少到 2 行代码(如果您使用 DbContext.Set&lt;T&gt; 而不是 DbSet 属性,则只有 1 行),然后调用模拟代码在包装器中。

要使用它,请将文件夹 MockHelpers 中的文件复制并包含到您的测试项目中。

这是一个使用上述内容的示例测试,请注意现在只需要 2 行代码即可在模拟的 DbContext 上设置模拟 DbSet&lt;T&gt;

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 模式的测试同样容易,例如 DbSet&lt;T&gt; 上的 .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

【讨论】:

【参考方案5】:

您不需要模拟 IQueryable 的所有部分。当我使用 NSubstitute 模拟 EF DbContext 时,我会这样做:

interface IContext

  IDbSet<Foo> Foos  get; set; 


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

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

通过一个简单的 IDbSet 实现围绕一个列表或我的 MockDbSet() 的东西。

一般来说,你应该模拟接口,而不是类型,因为 NSubstitute 只会覆盖虚拟方法。

【讨论】:

在 EF 6 DbSet 中,应将属性设为虚拟以实现可测试性。我不想创建一个假的 DbSet 包装器。替换这 4 个属性应该比创建您提议的包装器更容易和更好。所以你的建议并不是我想要的答案。 从错误消息来看,我会说 NSubstitute 创建的 DbSet 代理不支持显式实现的接口,仅支持您要替换的特定合同或对象。这是在 (github.com/nsubstitute/NSubstitute/issues/95) 中诞生的,可能是框架的限制。

以上是关于NSubstitute DbSet / IQueryable<T>的主要内容,如果未能解决你的问题,请参考以下文章

单元测试-隔离框架NSubstitute

用NSubstitute来mock

参数匹配不能与 NSubstitute 一起正常工作

NSubstitute ForPartsOf模拟除一个以外的所有方法?

使用 FluentAssertions 和 NSubstitute 的异步单元测试是不确定的

使用 NSubstitute 模拟出现问题时抛出异常的方法