NSubstitute В Dbset / Интерфейс IQueryable

таким образом, EntityFramework 6 намного лучше тестируется, чем предыдущие версии. И есть некоторые хорошие примеры в интернете для таких фреймворков, как Moq, но дело в том, что я предпочитаю использовать NSubstitute. У меня есть примеры" non-query", переведенные для работы с использованием NSubstitute, но я не могу обойти "query-test".

как 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); но это не сработало.

обман, который я получаю:

система".NotImplementedException: член ' IQueryable.Поставщик не был реализован на типе " 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)?

5 ответов


это происходит из-за специфического синтаксиса NSubstitute. Например в:

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

NSubstitute вызывает геттер поставщика, затем указывает возвращаемое значение. Этот вызов getter не перехватывается заменой, и вы получаете исключение. Это происходит из-за явной реализации IQueryable.Свойство поставщика в классе DbQuery.

вы можете явно создать заменители для нескольких интерфейсов с 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);
...

благодаря Кевину, я нашел проблему в моем переводе кода.

на unittest примеры кода над DbSet, но NSubstitute требует реализации интерфейса. Таким образом, эквивалент Moqs new Mock<DbSet<Blog>>() для NSubstitute является Substitute.For<IDbSet<Blog>>(). Вы не всегда должны предоставлять интерфейс, поэтому я был смущен. Но в данном конкретном случае это оказалось важным.

также оказалось, что нам не нужно бросать в Queryable, когда использование IDbSet интерфейса.

Итак, рабочий тестовый код:

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

}

я написал обертку около года назад вокруг того же кода, на который вы ссылаетесь из тестирование с помощью собственных тестовых двойников (EF6 и далее). Эту обертку можно найти на GitHub DbContextMockForUnitTests. Цель этой оболочки-уменьшить количество повторяющегося / дублирующего кода, необходимого для настройки модульных тестов, которые используют EF, где вы хотите издеваться над этим DbContext и DbSets. Большая часть макетного кода EF, который у вас есть в OP, может быть уменьшена до 2 строк код (и только 1, Если вы используете DbContext.Set<T> вместо свойств DbSet) и макет кода затем вызывается в оболочке.

чтобы использовать его, скопируйте и включите файлы в папку 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>.

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

С простой реализацией IDbSet вокруг списка или что-то для моего MockDbSet().

В общем случае вы должны издеваться над интерфейсами, а не над типами, поскольку NSubstitute будет переопределять только виртуальные методы.