c# - context - mocking entity framework when unit testing
Burlarse de EF DbContext con Moq (5)
Intento crear una prueba unitaria para mi servicio con un DbContext falso. IDbContext
una interfaz IDbContext
con las siguientes funciones:
public interface IDbContext : IDisposable
{
IDbSet<T> Set<T>() where T : class;
DbEntityEntry<T> Entry<T>(T entity) where T : class;
int SaveChanges();
}
Mi contexto real implementa esta interfaz IDbContext
y DbContext
.
Ahora estoy tratando de burlar el IDbSet<T>
en el contexto, por lo que devuelve una 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());
}
Siempre obtengo este error. .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
Basado en este artículo de MSDN , he creado mi propia biblioteca llamada EntityFrameworkMock
para burlarse de DbContext
y DbSet
. Disponible en NuGet y github.
Actualmente solo es EF6, porque eso es lo que estoy usando en este momento. Probablemente se pueda convertir en un proyecto multi-objetivo para también apuntar .NET Core.
El motivo por el que creé esta biblioteca es porque quería emular el comportamiento de SaveChanges
, lanzar una DbUpdateException
al insertar modelos con la misma clave primaria y admitir claves primarias de múltiples columnas / DbUpdateException
en los modelos.
Además, dado que tanto DbSetMock
como DbContextMock
heredan de Mock<DbSet>
y Mock<DbContext
, puede usar todas las funciones del framework Moq .
El uso se ve así:
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);
}
En caso de que alguien aún esté interesado, estaba teniendo el mismo problema y encontré este artículo muy útil: Entity Framework Testing with a Mocking Framework (EF6 en adelante)
Solo se aplica a Entity Framework 6 o posterior, pero cubre todo, desde simples pruebas de SaveChanges hasta pruebas de consulta asíncronas, todas usando Moq (y algunas de las clases manuales).
Gracias Gaui por tu gran idea =)
Agregué algunas mejoras a su solución y quiero compartirla.
- My
FakeDbSet
también inherents deDbSet
para obtener métodos adicionales comoAddRange()
-
ObservableCollection<T>
elObservableCollection<T>
conList<T>
para pasar todos los métodos ya implementados enList<>
a miFakeDbSet
Mi 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();
}
}
Es muy fácil modificar el dbSet y simular el objeto de contexto 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);
Ahora es posible ejecutar consultas de Linq, pero tenga en cuenta que las referencias de clave externa pueden no crearse automáticamente:
var user = contextMock.Object.Users.SingeOrDefault(userItem => userItem.Id == 42);
Como se burla del objeto de contexto, Context.SaveChanges()
no hará nada y los cambios de propiedad de sus entidades podrían no llenarse en su dbSet. Lo resolví burlándome de mi método SetModifed()
para completar los cambios.
Pude resolverlo creando una FakeDbSet<T>
que implementa 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();
}
}
Ahora mi prueba se ve así:
[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());
}
Si alguien todavía está buscando respuestas, he implementado una pequeña biblioteca para permitir el burlarse de DbContext.
paso 1
Instalar el paquete Coderful.EntityFramework.Testing Coderful.EntityFramework.Testing:
Install-Package Coderful.EntityFramework.Testing
paso 2
Luego crea una clase como esta:
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);
}
}
paso 3
Ahora puedes crear burlas muy fácilmente:
// 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;
Y luego usa tu simulacro:
// 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();
Artículo completo: http://www.22bugs.co/post/Mocking-DbContext/