Pruebas sin el sistema de base de datos de producción
En esta página, se describen técnicas para escribir pruebas automatizadas que no implican al sistema de base de datos en el que se ejecuta la aplicación en producción, intercambiando la base de datos por un doble de prueba. Hay varios tipos de dobles de prueba y enfoques para hacerlo, y se recomienda leer exhaustivamente el artículo Elección de una estrategia de prueba para comprender completamente las distintas opciones. Por último, también es posible probar contra su sistema de base de datos de producción; esto se aborda en Pruebas contra el sistema de base de datos de producción.
Sugerencia
En esta página se muestran técnicas de xUnit, pero existen conceptos similares en otros marcos de pruebas, como NUnit.
Patrón de repositorio
Si ha decidido escribir pruebas sin incluir el sistema de base de datos de producción, la técnica recomendada para hacerlo es el patrón de repositorio; para obtener más información sobre esto, vea esta sección. El primer paso para implementar el patrón de repositorio es extraer las consultas LINQ de EF Core en una capa independiente, que más adelante simularemos o utilizaremos como prototipo. Este es un ejemplo de una interfaz de repositorio para nuestro sistema de blogs.
public interface IBloggingRepository
{
Task<Blog> GetBlogByNameAsync(string name);
IAsyncEnumerable<Blog> GetAllBlogsAsync();
void AddBlog(Blog blog);
Task SaveChangesAsync();
}
... y esta es una implementación de ejemplo parcial para su uso en producción:
public class BloggingRepository : IBloggingRepository
{
private readonly BloggingContext _context;
public BloggingRepository(BloggingContext context)
=> _context = context;
public async Task<Blog> GetBlogByNameAsync(string name)
=> await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);
// Other code...
}
No hay mucho que destacar: el repositorio simplemente encapsula un contexto de EF Core y expone métodos que ejecutan las consultas y actualizaciones de base de datos en dicho contexto. Un punto clave que se debe tener en cuenta es que nuestro método GetAllBlogs
devuelve IAsyncEnumerable<Blog>
(o IEnumerable<Blog>
) y no IQueryable<Blog>
. La devolución de este último elemento significaría que los operadores de consulta todavía se pueden componer sobre el resultado, lo que requiere que EF Core siga participando en la traducción de la consulta; esto sería contrario al propósito de tener un repositorio en primer lugar. IAsyncEnumerable<Blog>
nos permite fácilmente crear stubs o simular lo que devuelve el repositorio.
Para una aplicación ASP.NET Core, es necesario registrar el repositorio como servicio en la inserción de dependencias agregando lo siguiente a la ConfigureServices
de la aplicación :
services.AddScoped<IBloggingRepository, BloggingRepository>();
Por último, nuestros controladores se insertan con el servicio de repositorio en lugar del contexto de EF Core y ejecutan métodos en él:
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public async Task<Blog> GetBlog(string name)
=> await _repository.GetBlogByNameAsync(name);
En este momento, la aplicación se diseña según el patrón de repositorio: el único punto de contacto con la capa de acceso a datos (EF Core) ahora se encuentra a través del nivel de repositorio, que actúa como mediador entre el código de la aplicación y las consultas de base de datos reales. Las pruebas ahora se pueden escribir simplemente mediante el código auxiliar del repositorio o simulándolo con su biblioteca de simulación favorita. Este es un ejemplo de una prueba basada en mock mediante la popular biblioteca Moq:
[Fact]
public async Task GetBlog()
{
// Arrange
var repositoryMock = new Mock<IBloggingRepository>();
repositoryMock
.Setup(r => r.GetBlogByNameAsync("Blog2"))
.Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));
var controller = new BloggingControllerWithRepository(repositoryMock.Object);
// Act
var blog = await controller.GetBlog("Blog2");
// Assert
repositoryMock.Verify(r => r.GetBlogByNameAsync("Blog2"));
Assert.Equal("http://blog2.com", blog.Url);
}
El código de ejemplo completo se puede ver aquí.
SQLite en memoria
SQLite se puede configurar fácilmente como proveedor de EF Core para el conjunto de pruebas en lugar del sistema de base de datos de producción (e.g. SQL Server); consulte la documentación del proveedor de SQLite para obtener más información. Pero normalmente es una buena idea usar la característica de base de datos en memoria de SQLite a la hora de realizar pruebas, ya que proporciona un aislamiento sencillo entre las pruebas y no requiere trabajar con archivos reales de SQLite.
Para usar SQLite en memoria, es importante comprender que se crea una nueva base de datos cada vez que se abre una conexión de bajo nivel y que se elimina cuando se cierra esa conexión. En el uso normal, el DbContext
de EF Core se abre y cierra las conexiones de base de datos según sea necesario (cada vez que se ejecuta una consulta) para evitar mantener la conexión durante tiempos innecesarios. Sin embargo, con SQLite en memoria esto provocaría restablecer la base de datos cada vez; por lo que, como solución alternativa, se abre la conexión antes de pasarla a EF Core y se organiza para que se cierre solo cuando se complete la prueba:
public SqliteInMemoryBloggingControllerTest()
{
// Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
// at the end of the test (see Dispose below).
_connection = new SqliteConnection("Filename=:memory:");
_connection.Open();
// These options will be used by the context instances in this test suite, including the connection opened above.
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlite(_connection)
.Options;
// Create the schema and seed some data
using var context = new BloggingContext(_contextOptions);
if (context.Database.EnsureCreated())
{
using var viewCommand = context.Database.GetDbConnection().CreateCommand();
viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
viewCommand.ExecuteNonQuery();
}
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
BloggingContext CreateContext() => new BloggingContext(_contextOptions);
public void Dispose() => _connection.Dispose();
Las pruebas ahora pueden llamar a CreateContext
, que devuelve un contexto usando la conexión que hemos configurado en el constructor, lo que garantiza que tenemos una base de datos limpia con los datos inicializados.
El código de ejemplo completo para las pruebas en memoria de SQLite se puede ver aquí.
Proveedor en memoria
Como se describe en la página de información general de pruebas de , se desaconseja encarecidamente usar el proveedor en memoria para las pruebas; considere la posibilidad de usar SQLite en su lugaro bien implementar el patrón de repositorio. Si ha decidido usar en memoria, este es un constructor de clase de prueba típico que configura y inicializa una nueva base de datos en memoria antes de cada prueba:
public InMemoryBloggingControllerTest()
{
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseInMemoryDatabase("BloggingControllerTest")
.ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
using var context = new BloggingContext(_contextOptions);
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
El código de ejemplo completo para las pruebas en memoria se puede ver aquí.
Nomenclatura de la base de datos en memoria
Las bases de datos en memoria se identifican mediante un nombre de cadena simple y es posible conectarse a la misma base de datos varias veces proporcionando el mismo nombre (por lo que el ejemplo anterior debe llamar a EnsureDeleted
antes de cada prueba). Sin embargo, tenga en cuenta que las bases de datos en memoria se basan en el proveedor de servicios interno del contexto; aunque en la mayoría de los casos, los contextos comparten el mismo proveedor de servicios, la configuración de contextos con diferentes opciones puede desencadenar el uso de un nuevo proveedor de servicios interno. Cuando ese es el caso, pase explícitamente la misma instancia de InMemoryDatabaseRoot a UseInMemoryDatabase
para todos los contextos que deben compartir bases de datos en memoria (normalmente se hace con un campo de InMemoryDatabaseRoot
estático).
Transacciones
Tenga en cuenta que, de forma predeterminada, si se inicia una transacción, el proveedor en memoria producirá una excepción, ya que no se admiten transacciones. Es posible que desee que las transacciones se omitan silenciosamente en su lugar, configurando EF Core para omitir InMemoryEventId.TransactionIgnoredWarning
como en el ejemplo anterior. No obstante, si el código realmente se basa en la semántica transaccional, por ejemplo, si depende de la reversión real de los cambios, la prueba no funcionará.
Vistas
El proveedor en memoria permite la definición de vistas por medio de consultas LINQ mediante el uso de ToInMemoryQuery:
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));