NSubstitute DbSet / IQueryable<T>

a 夏天 提交于 2019-11-27 11:28:07

This happens because of NSubstitute syntax specific. For example in:

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

NSubstitute calls the Provider's getter, then it specifies the return value. This getter call isn't intercepted by the substitute and you get an exception. It happens because of explicit implementation of IQueryable.Provider property in DbQuery class.

You can explicitly create substitutes for multiple interfaces with NSub, and it creates a proxy which covers all specified interfaces. Then calls to the interfaces will be intercepted by the substitute. Please use the following syntax:

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

Thanks to Kevin, I've found the problem in my code translation.

The unittest code samples are mocking DbSet, but NSubstitute requires the interface implementation. So the equivalent of Moqs new Mock<DbSet<Blog>>() for NSubstitute is Substitute.For<IDbSet<Blog>>(). You're not always required to provide the Interface, so that's why I was confused. But in this specific case, it turned out to be crucial.

It also turned out that we don't have to cast to Queryable when using the interface IDbSet.

So the working test code:

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 ...
}

I've written a small extention method to cleanup the Arrange section of the unit tests.

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

Not the question, but in case you also need to be able to support async operations:

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

This is my static generic static method to generate fake DbSet. It may by useful.

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

}

I wrote a wrapper about a year ago around the same code you are referencing from Testing with Your Own Test Doubles (EF6 onwards). This wrapper can be found on GitHub DbContextMockForUnitTests. The purpose of this wrapper is to reduce the amount of repetitive/duplicate code needed to setup unit tests that make use of EF where you want to mock that DbContext and DbSets. Most of the mock EF code you have in the OP can reduced down to a 2 lines of code (and only 1 if you are using DbContext.Set<T> instead of DbSet properties) and the mock code is then called in the wrapper.

To use it copy and include the files in folder MockHelpers to your Test project.

Here is an example test using what you had above, notice that there is now only 2 Lines of code are needed to setup the mock DbSet<T> on the 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
}

It is just as easy to make this a test that invokes something that uses the async/await pattern like .ToListAsync() on the 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
}

You shouldn't need to mock all of the pieces of the IQueryable. When I use NSubstitute for mocking an EF DbContext I do something like so:

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

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

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

With a simple implementation of IDbSet around a list or something for my MockDbSet().

In general you should be mocking interfaces, not types as NSubstitute will only override virtual methods.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!