NSubstitute DbSet/IQueryable <T>

s.m*_*jer 36 c# entity-framework nsubstitute dbcontext dbset

因此,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);

  // ...
}
Run Code Online (Sandbox Code Playgroud)

这就是我与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);

  // ...
}
Run Code Online (Sandbox Code Playgroud)

所以问题是; 如何替换IQueryable的属性(如Provider)?

Ale*_*tin 38

这是因为NSubstitute语法特定.例如:

((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
Run Code Online (Sandbox Code Playgroud)

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);
...
Run Code Online (Sandbox Code Playgroud)

  • 提示:确保你的上下文中的`DbSet`是`virtual` (4认同)
  • 是否有一个优雅的解决方案来处理`.Include()`问题?我不断收到“源为空”错误。 (2认同)

s.m*_*jer 17

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

单元测试代码示例是嘲讽DbSet,但NSubstitute需要的接口实现.因此相当于NSubstitute new Mock<DbSet<Blog>>()的Moqs 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 ...
}
Run Code Online (Sandbox Code Playgroud)

我写了一个小的扩展方法来清理单元测试的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);
Run Code Online (Sandbox Code Playgroud)

不是问题,但如果您还需要能够支持异步操作:

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);
Run Code Online (Sandbox Code Playgroud)

  • 我试过这个但得到一个异常:NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException:无法为BloggingContext.get_Blogs返回类型IDbSet`1Proxy的值(期望类型DbSet`1).Context由EF TT模板生成 (2认同)

小智 5

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

}
Run Code Online (Sandbox Code Playgroud)