41

所以 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 '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)?

4

5 回答 5

42

发生这种情况是因为 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());
于 2014-01-12T14:06:23.480 回答
19

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

unittest 代码示例是 mocking ,DbSet但 NSubstitute 需要接口实现。所以 NSubstitutenew 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);
于 2014-01-12T12:34:41.583 回答
6

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

}
于 2015-08-27T11:25:11.117 回答
3

大约一年前,我围绕您从使用您自己的测试替身进行测试(EF6 起)中引用的相同代码编写了一个包装器。这个包装器可以在GitHub DbContextMockForUnitTests上找到。此包装器的目的是减少设置单元测试所需的重复/重复代码的数量,这些单元测试在您想要模拟的地方使用 EFDbContextDbSets. 您在 OP 中拥有的大多数模拟 EF 代码可以减少到 2 行代码(如果您使用DbContext.Set<T>而不是 DbSet 属性,则只有 1 行),然后在包装器中调用模拟代码。

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

这是一个使用上面的示例测试,请注意现在只需要 2 行代码即可DbSet<T>在 mocked 上设置模拟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>.

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
}
于 2016-09-14T11:33:12.863 回答
0

您不需要模拟 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 只会覆盖虚拟方法。

于 2014-01-12T01:15:19.440 回答