Una de las preguntas más recurrentes en internet es como crear Unit Test dentro de Entity Framework cuando utilizamos DBcontext.
Índice
Como siempre, todo el código está disponible en GitHub.
1 - Unit test o Integration tests en entity framework
En este post no voy a entrar en si debemos crear test o no, obviamente si, lo he explicado ya mil veces. En este punto quiero adentrarme en la diferencia que vamos a ver entre unit test e integration tests.
Empecemos con los test de integración, en mi opinión deben utilizar bases de datos reales, servicios reales, etc, puedes ahorrarte o simular la respuesta de sistemas de terceros, pero algo básico como la base de datos, debemos utilizarla, ya sea levantando la misma en docker o utilizando una real, pero NO simules en memoria ni cosas parecidas.
Para los test unitarios o test de componentes es donde simulamos en memoria, o hacemos un fake, doble o moq, ahí sí están permitidas, al final la idea de los test unitarios para testear funcionalidades mínimas, dependerá de la dificultad o complejidad que quieras incluir en tus tests si haces test unitarios mínimos o test de componentes.
Para mi, un test de componentes es un test que testea una funcionalidad completa, estando completamente en memoria, por lo tanto en nuestro caso, no incluye uso de una base de datos real.
2 - Cómo crear test unitarios con patrón repositorio
El primer punto lo voy a pasar bien rápido, si utilizamos patrón repositorio o unidad de trabajo podemos hacer mock, fake o stub de las interfaces. Así que no tiene más misterio, coges moq, NSubstitute o FakeItEasy y simular el funcionamiento de las interfaces.
3 - Mock DbContext de Entity Framework
Si lo que queremos es hacer un mock del Dbcontext, es algo más complicado, pero tampoco mucho.
Dependerá principalmente de que librería queramos usar para los mocks. Si estamos usando moq, debemos instalar moq.EntityFrameworkCore, si estamos utilizando NSubstitute instalaremos MockQueryable.NSubstitute
y si estamos utilizando FakeItEasy utilizaremos la librería MockQueryable.FakeItEasy
, moq, también tiene MockQueryable.Moq
.
Para este ejemplo, voy a utilizar NSubstitue y por lo tanto MockQueryable.NSubstitute
.
Primero debemos ponernos en el caso de uso, que en nuestro caso es insertar un usuario, este ejemplo lo vimos en el pasado utilizando unit of work, pero como digo, aquí utilizaremos DbContext
directamente:
public class InsertUserDbContext
{
private readonly CursoEfContext _context;
public InsertUserDbContext(CursoEfContext context)
{
_context = context;
}
public async Task<bool> Execute(int id)
{
User user = new User()
{
Email = $"{Guid.NewGuid()}@mail.com",
UserName = $"id{id}"
};
List<Wokringexperience> workingExperiences = new List<Wokringexperience>()
{
new Wokringexperience()
{
User = user,
Name = $"experience1 user {id}",
Details = "details1",
Environment = "environment"
},
new Wokringexperience()
{
User = user,
Name = $"experience user {id}",
Details = "details2",
Environment = "environment"
}
};
if (_context.Users.Any(a.UserName == user.UserName))
return false;
_ = await _context.Users.AddAsync(user);
await _context.Wokringexperiences.AddRangeAsync(workingExperiences);
_ = await _context.SaveChangesAsync();
return true;
}
}
Ahora únicamente nos queda crear el test, donde vamos a simular el dbset, antes de continuar, como he dicho, simular un DbContext es complicadillo, pero este método te permitirá simular perfectamente el funcionamiento de un DbSet con NSubstitute:
private 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.AsQueryable().Returns(fakeDbSet);
return fakeDbSet;
}
Y ya solo tenemos que escribir el test, dónde estamos utilizando NSubstitute en el Dbset del dBcontext para devolver el que hemos creado.
[Fact]
public async Task WhenUsernameDoesNotExist_ThenUserInserted()
{
CursoEfContext context = Substitute.For<CursoEfContext>(new DbContextOptions<CursoEfContext>());
DbSet<User> userDbSet = FakeDbSet(new List<User>());
context.Users.Returns(userDbSet);
DbSet<Wokringexperience> dbSetWorkingExperiences = FakeDbSet(new List<Wokringexperience>());
context.Wokringexperiences.Returns(dbSetWorkingExperiences);
InsertUserDbContext subject = new InsertUserDbContext(context);
var result = await subject.Execute(10);
Assert.True(result);
}
Como vemos muy sencillito, por supuesto, si incluimos un elemento en una de las listas, se comprobará sobre el que hemos metido nosotros:
[Fact]
public async Task WhenUsernameDoesExist_ThenNotInserted()
{
int id = 1;
CursoEfContext context = Substitute.For<CursoEfContext>(new DbContextOptions<CursoEfContext>());
DbSet<User> userDbSet = FakeDbSet(new List<User>() { new User() { UserName = $"id{id}" } });
context.Users.Returns(userDbSet);
InsertUserDbContext subject = new InsertUserDbContext(context);
var result = await subject.Execute(id);
Assert.False(result);
}
Como recordamos el caso de uso comprueba si un username existe o no.
4 - DbContext en memoria para test de componentes
En el punto anterior hemos visto cómo hacer mock de nuestro DbContext, pero muchas veces simplemente queremos simular en memoria. Para ello en nuestro test, tenemos que utilizar el método UseInMemoryDatabase
cuando definimos el dbcontext:
private CursoEfContext GetInMemoryDbContext()
{
DbContextOptions<CursoEfContext> databaseOptions = new DbContextOptionsBuilder<CursoEfContext>()
.UseInMemoryDatabase("CursoDatabase")
.Options;
return new CursoEfContext(databaseOptions);
}
Con esto, simplificamos la funcionalidad un montón ya que es una única instrucción y funciona perfectamente, ademas, esta base de datos en memoria, es muy común y muy recomendable de utilizar si hacemos test de componentes que incluyen la bbdd o estamos utilizando test de API con testserver, ya que funciona a la perfección.
Ahora únicamente vamos a escribir los mismos tests que hemos creado en el punto anterior para así ver la simplicidad de la que hablo:
[Fact]
public async Task WhenUsernameDoesNotExist_ThenUserInsertedInDatabase()
{
CursoEfContext context = GetInMemoryDbContext();
InsertUserDbContext subject = new InsertUserDbContext(context);
var result = await subject.Execute(10);
Assert.True(result);
}
Como vemos, no estamos simulando ningún funcionamiento y todo funciona a la perfección.
Por supuesto, podemos crear operaciones CRUD en dicho DbContext en el propio test:
[Fact]
public async Task WhenUsernameDoesExist_ThenNotInsertedInDatabase() //inMemory
{
int id = 1;
CursoEfContext context = GetInMemoryDbContext();
await context.Users.AddAsync(new User() { UserName = $"id{id}", Email = "[email protected]"});
await context.SaveChangesAsync();
InsertUserDbContext subject = new InsertUserDbContext(context);
var result = await subject.Execute(id);
Assert.False(result);
}
Antes de terminar, y quiero dejar esto claro, aquí estamos utilizando una base de datos en memoria, lo que quiere decir que no estamos atacando una base de datos real. Por lo que si estamos utilizando una funcionalidad que no existe en dicha base de datos, pero si existe en entity framework ,nos va a funcionar en los test pero no en producción.
Por ese motivo, SIEMPRE debemos hacer test de integración de cada uno de nuestros happy paths.