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.Поставщик не был реализован на типе " 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'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 будет переопределять только виртуальные методы.