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