Подтвердить что ты не робот

NSubstitute DbSet/IQueryable <T>

Итак, 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 'DbSet 1'. Тестирование двойников для" 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 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 (например, поставщика)?

4b9b3361

Ответ 1

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

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

NSubstitute вызывает провайдер-получатель, затем он указывает возвращаемое значение. Этот getter-вызов не перехвачен заменой, и вы получаете исключение. Это происходит из-за явной реализации свойства IQueryable.Provider в классе 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);
...

Ответ 2

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

unittest code samples издеваются над DbSet, но NSubstitute требует реализации интерфейса. Таким образом, эквивалент Moqs new Mock<DbSet<Blog>>() для NSubstitute равен 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 ...
}

Я написал небольшой метод расширения для очистки раздела 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);

Не вопрос, но в случае, если вам также необходимо поддерживать асинхронные операции:

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

Ответ 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;
    }

}

Ответ 4

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

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

Ответ 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 будет только переопределять виртуальные методы.