Mocking EF DbContext с Moq
Я пытаюсь создать unit test для моего сервиса с издеваемым DbContext. Я создал интерфейс IDbContext
со следующими функциями:
public interface IDbContext : IDisposable
{
IDbSet<T> Set<T>() where T : class;
DbEntityEntry<T> Entry<T>(T entity) where T : class;
int SaveChanges();
}
Мой реальный контекст реализует этот интерфейс IDbContext
и DbContext
.
Теперь я пытаюсь высмеять IDbSet<T>
в контексте, поэтому он возвращает вместо List<User>
.
[TestMethod]
public void TestGetAllUsers()
{
// Arrange
var mock = new Mock<IDbContext>();
mock.Setup(x => x.Set<User>())
.Returns(new List<User>
{
new User { ID = 1 }
});
UserService userService = new UserService(mock.Object);
// Act
var allUsers = userService.GetAllUsers();
// Assert
Assert.AreEqual(1, allUsers.Count());
}
Я всегда получаю эту ошибку на .Returns
:
The best overloaded method match for
'Moq.Language.IReturns<AuthAPI.Repositories.IDbContext,System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>.Returns(System.Func<System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>)'
has some invalid arguments
Ответы
Ответ 1
Мне удалось решить его, создав класс FakeDbSet<T>
, который реализует IDbSet<T>
public class FakeDbSet<T> : IDbSet<T> where T : class
{
ObservableCollection<T> _data;
IQueryable _query;
public FakeDbSet()
{
_data = new ObservableCollection<T>();
_query = _data.AsQueryable();
}
public virtual T Find(params object[] keyValues)
{
throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
}
public T Add(T item)
{
_data.Add(item);
return item;
}
public T Remove(T item)
{
_data.Remove(item);
return item;
}
public T Attach(T item)
{
_data.Add(item);
return item;
}
public T Detach(T item)
{
_data.Remove(item);
return item;
}
public T Create()
{
return Activator.CreateInstance<T>();
}
public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
{
return Activator.CreateInstance<TDerivedEntity>();
}
public ObservableCollection<T> Local
{
get { return _data; }
}
Type IQueryable.ElementType
{
get { return _query.ElementType; }
}
System.Linq.Expressions.Expression IQueryable.Expression
{
get { return _query.Expression; }
}
IQueryProvider IQueryable.Provider
{
get { return _query.Provider; }
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _data.GetEnumerator();
}
}
Теперь мой тест выглядит следующим образом:
[TestMethod]
public void TestGetAllUsers()
{
//Arrange
var mock = new Mock<IDbContext>();
mock.Setup(x => x.Set<User>())
.Returns(new FakeDbSet<User>
{
new User { ID = 1 }
});
UserService userService = new UserService(mock.Object);
// Act
var allUsers = userService.GetAllUsers();
// Assert
Assert.AreEqual(1, allUsers.Count());
}
Ответ 2
Спасибо Гауи за твою прекрасную идею =)
Я добавил некоторые улучшения в ваше решение и хочу поделиться им.
- Мой
FakeDbSet
также присущ от DbSet
для получения дополнительных методов
например AddRange()
- Я заменил
ObservableCollection<T>
на List<T>
, чтобы передать все
уже реализованные методы в List<>
до моего FakeDbSet
My FakeDbSet:
public class FakeDbSet<T> : DbSet<T>, IDbSet<T> where T : class {
List<T> _data;
public FakeDbSet() {
_data = new List<T>();
}
public override T Find(params object[] keyValues) {
throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
}
public override T Add(T item) {
_data.Add(item);
return item;
}
public override T Remove(T item) {
_data.Remove(item);
return item;
}
public override T Attach(T item) {
return null;
}
public T Detach(T item) {
_data.Remove(item);
return item;
}
public override T Create() {
return Activator.CreateInstance<T>();
}
public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T {
return Activator.CreateInstance<TDerivedEntity>();
}
public List<T> Local {
get { return _data; }
}
public override IEnumerable<T> AddRange(IEnumerable<T> entities) {
_data.AddRange(entities);
return _data;
}
public override IEnumerable<T> RemoveRange(IEnumerable<T> entities) {
for (int i = entities.Count() - 1; i >= 0; i--) {
T entity = entities.ElementAt(i);
if (_data.Contains(entity)) {
Remove(entity);
}
}
return this;
}
Type IQueryable.ElementType {
get { return _data.AsQueryable().ElementType; }
}
Expression IQueryable.Expression {
get { return _data.AsQueryable().Expression; }
}
IQueryProvider IQueryable.Provider {
get { return _data.AsQueryable().Provider; }
}
IEnumerator IEnumerable.GetEnumerator() {
return _data.GetEnumerator();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator() {
return _data.GetEnumerator();
}
}
Очень легко изменить dbSet и Mock объект контекста EF:
var userDbSet = new FakeDbSet<User>();
userDbSet.Add(new User());
userDbSet.Add(new User());
var contextMock = new Mock<MySuperCoolDbContext>();
contextMock.Setup(dbContext => dbContext.Users).Returns(userDbSet);
Теперь можно выполнить запросы Linq, но знайте, что ссылки на внешние ключи могут не создаваться автоматически:
var user = contextMock.Object.Users.SingeOrDefault(userItem => userItem.Id == 42);
Поскольку объект контекста высмеивается, Context.SaveChanges()
ничего не сделает, и изменения свойств ваших объектов не могут быть заполнены на ваш dbSet. Я решил это, высмеивая мой метод SetModifed()
для заполнения изменений.
Ответ 3
Если кому-то все еще интересно, у меня возникла такая же проблема, и я нашел эту статью очень полезной:
Тестирование платформы Entity с помощью Mocking Framework (EF6 и далее)
Он применяется только к Entity Framework 6 или новее, но он охватывает все, от простых тестов SaveChanges до тестирования асинхронного запроса, используя Moq (и несколько ручных классов).
Ответ 4
Если кто-то все еще ищет ответы, я внедрил небольшую библиотеку чтобы разрешить насмешливый DbContext.
шаг 1
Установите Coderful.EntityFramework.Testing пакет nuget:
Install-Package Coderful.EntityFramework.Testing
шаг 2
Затем создайте класс следующим образом:
internal static class MyMoqUtilities
{
public static MockedDbContext<MyDbContext> MockDbContext(
IList<Contract> contracts = null,
IList<User> users = null)
{
var mockContext = new Mock<MyDbContext>();
// Create the DbSet objects.
var dbSets = new object[]
{
MoqUtilities.MockDbSet(contracts, (objects, contract) => contract.ContractId == (int)objects[0] && contract.AmendmentId == (int)objects[1]),
MoqUtilities.MockDbSet(users, (objects, user) => user.Id == (int)objects[0])
};
return new MockedDbContext<SourcingDbContext>(mockContext, dbSets);
}
}
шаг 3
Теперь вы можете легко создавать mocks:
// Create test data.
var contracts = new List<Contract>
{
new Contract("#1"),
new Contract("#2")
};
var users = new List<User>
{
new User("John"),
new User("Jane")
};
// Create DbContext with the predefined test data.
var dbContext = MyMoqUtilities.MockDbContext(
contracts: contracts,
users: users).DbContext.Object;
И затем используйте свой макет:
// Create.
var newUser = dbContext.Users.Create();
// Add.
dbContext.Users.Add(newUser);
// Remove.
dbContext.Users.Remove(someUser);
// Query.
var john = dbContext.Users.Where(u => u.Name == "John");
// Save changes won't actually do anything, since all the data is kept in memory.
// This should be ideal for unit-testing purposes.
dbContext.SaveChanges();
Полная статья: http://www.22bugs.co/post/Mocking-DbContext/
Ответ 5
Основываясь на этой статье MSDN, я создал свои собственные библиотеки для насмешек DbContext
и DbSet
:
- EntityFrameworkMock - GitHub
- EntityFrameworkMockCore - GitHub
Оба доступны на NuGet и GitHub.
Причина, по которой я создал эти библиотеки, заключается в том, что я хотел эмулировать поведение SaveChanges
, DbUpdateException
при вставке моделей с одинаковым первичным ключом и поддерживать первичные ключи с несколькими столбцами/автоинкрементами в моделях.
Кроме того, поскольку DbSetMock
и DbContextMock
наследуются от Mock<DbSet>
и Mock<DbContext
, вы можете использовать все функции инфраструктуры Moq.
Рядом с Moq также есть реализация NSubstitute.
Использование с версией Moq выглядит так:
public class User
{
[Key, Column(Order = 0)]
public Guid Id { get; set; }
public string FullName { get; set; }
}
public class TestDbContext : DbContext
{
public TestDbContext(string connectionString)
: base(connectionString)
{
}
public virtual DbSet<User> Users { get; set; }
}
[TestFixture]
public class MyTests
{
var initialEntities = new[]
{
new User { Id = Guid.NewGuid(), FullName = "Eric Cartoon" },
new User { Id = Guid.NewGuid(), FullName = "Billy Jewel" },
};
var dbContextMock = new DbContextMock<TestDbContext>("fake connectionstring");
var usersDbSetMock = dbContextMock.CreateDbSetMock(x => x.Users, initialEntities);
// Pass dbContextMock.Object to the class/method you want to test
// Query dbContextMock.Object.Users to see if certain users were added or removed
// or use Mock Verify functionality to verify if certain methods were called: usersDbSetMock.Verify(x => x.Add(...), Times.Once);
}
Ответ 6
Ответ @Szuuken выше fooobar.com/info/111916/... решил мою проблему с насмешкой над функцией EF.Add() в ASP.NET MVC 5. Хотя я знаю, что она не связана с Core, мне удалось извлечь достаточно информации, чтобы наследовать существующий класс, который дразнит dbset с помощью dbset, idbset и переопределяет метод Add. Спасибо!