Pruebas con InMemoryTesting with InMemory

El proveedor de inmemory es útil cuando se desea probar los componentes mediante algo que se aproxima a la conexión a la base de datos real, sin la sobrecarga de las operaciones de base de datos reales.The InMemory provider is useful when you want to test components using something that approximates connecting to the real database, without the overhead of actual database operations.

Sugerencia

Puede ver un ejemplo de este artículo en GitHub.You can view this article's sample on GitHub.

La inmemory no es una base de datos relacionalInMemory is not a relational database

EF Core proveedores de bases de datos no tienen que ser bases de datos relacionales.EF Core database providers do not have to be relational databases. La inmemory está diseñada para ser una base de datos de uso general para las pruebas y no está diseñada para imitar una base de datos relacional.InMemory is designed to be a general purpose database for testing, and is not designed to mimic a relational database.

Algunos ejemplos de esto son:Some examples of this include:

  • La inmemory le permitirá guardar datos que infrinjan las restricciones de integridad referencial en una base de datos relacional.InMemory will allow you to save data that would violate referential integrity constraints in a relational database.
  • Si usa DefaultValueSql (String) para una propiedad en el modelo, se trata de una API de base de datos relacional y no tendrá ningún efecto cuando se ejecute en inmemory.If you use DefaultValueSql(string) for a property in your model, this is a relational database API and will have no effect when running against InMemory.
  • No se admite la simultaneidad mediante la versión de marca de tiempo o fila ([Timestamp] o IsRowVersion).Concurrency via Timestamp/row version ([Timestamp] or IsRowVersion) is not supported. No se producirá DbUpdateConcurrencyException si se realiza una actualización mediante un token de simultaneidad antiguo.No DbUpdateConcurrencyException will be thrown if an update is done using an old concurrency token.

Sugerencia

Para muchos propósitos de prueba, estas diferencias no serán importantes.For many test purposes these differences will not matter. Sin embargo, si desea probar algo que se comporta más como una verdadera base de datos relacional, considere el uso del modo en memoria de SQLite.However, if you want to test against something that behaves more like a true relational database, then consider using SQLite in-memory mode.

Escenario de prueba de ejemploExample testing scenario

Considere el siguiente servicio que permite al código de la aplicación realizar algunas operaciones relacionadas con blogs.Consider the following service that allows application code to perform some operations related to blogs. Utiliza internamente un DbContext que se conecta a una base de datos SQL Server.Internally it uses a DbContext that connects to a SQL Server database. Sería útil intercambiar este contexto para conectarse a una base de datos inmemory de modo que podamos escribir pruebas eficaces para este servicio sin tener que modificar el código, o hacer mucho trabajo para crear un doble de prueba del contexto.It would be useful to swap this context to connect to an InMemory database so that we can write efficient tests for this service without having to modify the code, or do a lot of work to create a test double of the context.

using System.Collections.Generic;
using System.Linq;

namespace BusinessLogic
{
    public class BlogService
    {
        private BloggingContext _context;

        public BlogService(BloggingContext context)
        {
            _context = context;
        }

        public void Add(string url)
        {
            var blog = new Blog { Url = url };
            _context.Blogs.Add(blog);
            _context.SaveChanges();
        }

        public IEnumerable<Blog> Find(string term)
        {
            return _context.Blogs
                .Where(b => b.Url.Contains(term))
                .OrderBy(b => b.Url)
                .ToList();
        }
    }
}

Preparar el contextoGet your context ready

Evite configurar dos proveedores de bases de datosAvoid configuring two database providers

En las pruebas, va a configurar externamente el contexto para usar el proveedor de inmemory.In your tests you are going to externally configure the context to use the InMemory provider. Si está configurando un proveedor de base de datos invalidando OnConfiguring en el contexto, deberá agregar código condicional para asegurarse de que solo se configura el proveedor de base de datos si aún no se ha configurado ninguno.If you are configuring a database provider by overriding OnConfiguring in your context, then you need to add some conditional code to ensure that you only configure the database provider if one has not already been configured.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;ConnectRetryCount=0");
    }
}

Sugerencia

Si usa ASP.NET Core, no necesitará este código ya que el proveedor de base de datos ya está configurado fuera del contexto (en Startup.cs).If you are using ASP.NET Core, then you should not need this code since your database provider is already configured outside of the context (in Startup.cs).

Agregar un constructor para las pruebasAdd a constructor for testing

La manera más sencilla de habilitar las pruebas en una base de datos diferente es modificar el contexto para exponer un constructor que acepta un DbContextOptions<TContext>.The simplest way to enable testing against a different database is to modify your context to expose a constructor that accepts a DbContextOptions<TContext>.

public class BloggingContext : DbContext
{
    public BloggingContext()
    { }

    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    { }

Sugerencia

DbContextOptions<TContext> indica al contexto toda su configuración, como la base de datos a la que se va a conectar.DbContextOptions<TContext> tells the context all of its settings, such as which database to connect to. Este es el mismo objeto que se genera al ejecutar el método de configuración en el contexto.This is the same object that is built by running the OnConfiguring method in your context.

Escribir pruebasWriting tests

La clave para probar con este proveedor es la capacidad de indicar al contexto que use el proveedor de inmemory y controlar el ámbito de la base de datos en memoria.The key to testing with this provider is the ability to tell the context to use the InMemory provider, and control the scope of the in-memory database. Normalmente, desea una base de datos limpia para cada método de prueba.Typically you want a clean database for each test method.

Este es un ejemplo de una clase de prueba que usa la base de datos inmemory.Here is an example of a test class that uses the InMemory database. Cada método de prueba especifica un nombre de base de datos único, lo que significa que cada método tiene su propia base de datos inmemory.Each test method specifies a unique database name, meaning each method has its own InMemory database.

Sugerencia

Para usar el método de extensión .UseInMemoryDatabase(), haga referencia al paquete NuGet Microsoft. EntityFrameworkCore. inmemory.To use the .UseInMemoryDatabase() extension method, reference the NuGet package Microsoft.EntityFrameworkCore.InMemory.

using BusinessLogic;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Xunit;

namespace EFTesting.TestProject.InMemory
{
    public class BlogServiceTests
    {
        [Fact]
        public void Add_writes_to_database()
        {
            var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseInMemoryDatabase(databaseName: "Add_writes_to_database")
                .Options;

            // Run the test against one instance of the context
            using (var context = new BloggingContext(options))
            {
                var service = new BlogService(context);
                service.Add("https://example.com");
                context.SaveChanges();
            }

            // Use a separate instance of the context to verify correct data was saved to database
            using (var context = new BloggingContext(options))
            {
                Assert.Equal(1, context.Blogs.Count());
                Assert.Equal("https://example.com", context.Blogs.Single().Url);
            }
        }

        [Fact]
        public void Find_searches_url()
        {
            var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseInMemoryDatabase(databaseName: "Find_searches_url")
                .Options;

            // Insert seed data into the database using one instance of the context
            using (var context = new BloggingContext(options))
            {
                context.Blogs.Add(new Blog { Url = "https://example.com/cats" });
                context.Blogs.Add(new Blog { Url = "https://example.com/catfish" });
                context.Blogs.Add(new Blog { Url = "https://example.com/dogs" });
                context.SaveChanges();
            }

            // Use a clean instance of the context to run the test
            using (var context = new BloggingContext(options))
            {
                var service = new BlogService(context);
                var result = service.Find("cat");
                Assert.Equal(2, result.Count());
            }
        }
    }
}