Testowanie za pomocą InMemoryTesting with InMemory

Dostawca InMemory jest przydatne, gdy chcesz przetestować składników za pomocą coś, co przybliża z bazą danych rzeczywistych, bez konieczności operacji bazy danych.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.

Porada

Przykład użyty w tym artykule można zobaczyć w witrynie GitHub.You can view this article's sample on GitHub.

InMemory nie jest relacyjnej bazy danychInMemory is not a relational database

Dostawcy baz danych programu EF Core nie trzeba być relacyjnych baz danych.EF Core database providers do not have to be relational databases. InMemory została zaprojektowana jako bazy danych ogólnego przeznaczenia do testowania i nie jest przeznaczony do naśladowania relacyjnej bazy danych.InMemory is designed to be a general purpose database for testing, and is not designed to mimic a relational database.

Niektóre przykłady to między innymi:Some examples of this include:

  • InMemory pozwoli na zapisywanie danych, który mógłby naruszyć ograniczenia integralności referencyjnej w relacyjnej bazie danych.InMemory will allow you to save data that would violate referential integrity constraints in a relational database.
  • Jeśli używasz DefaultValueSql(string) właściwości w modelu jest interfejs API relacyjnej bazy danych i nie odniesie skutku przy uruchamianiu 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.
  • Współbieżność za pośrednictwem wiersza i znacznik czasu: wersja ([Timestamp] lub IsRowVersion) nie jest obsługiwane.Concurrency via Timestamp/row version ([Timestamp] or IsRowVersion) is not supported. Nie DbUpdateConcurrencyException zostanie zgłoszony, jeśli aktualizacja jest wykonywane przy użyciu starego tokenu współbieżności.No DbUpdateConcurrencyException will be thrown if an update is done using an old concurrency token.

Porada

Różnice te nie będą znaczenia, dla wielu celów testowych.For many test purposes these differences will not matter. Jednak jeśli chcesz przetestować względem elementu, który zachowuje się bardziej jak true relacyjnej bazy danych, rozważ użycie trybie w pamięci z bazy danych SQLite.However, if you want to test against something that behaves more like a true relational database, then consider using SQLite in-memory mode.

Przykładowy scenariusz testowaniaExample testing scenario

Należy wziąć pod uwagę następujące usługa, która umożliwia wykonywanie operacji związanych z blogów kodu aplikacji.Consider the following service that allows application code to perform some operations related to blogs. Używa wewnętrznie DbContext łączący się z bazą danych programu SQL Server.Internally it uses a DbContext that connects to a SQL Server database. Należałoby wymiany z tym kontekstem do nawiązania połączenia z bazą InMemory tak, aby firma Microsoft można zapisać efektywne testy dla tej usługi bez konieczności modyfikowania kodu lub wykonać dużo pracy, aby utworzyć test podwójnego kontekstu.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();
        }
    }
}

Przygotuj kontekstuGet your context ready

Należy unikać konfigurowania dwóch dostawców bazy danychAvoid configuring two database providers

W testach ma zewnętrznie skonfigurować kontekst do użycia dostawcy InMemory.In your tests you are going to externally configure the context to use the InMemory provider. Jeśli konfigurujesz dostawcy bazy danych przez zastąpienie OnConfiguring w kontekstu, następnie należy dodać niektórych kod warunkowy, aby upewnić się, można tylko skonfigurować dostawcy bazy danych, gdy nie została już skonfigurowana.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");
    }
}

Porada

Jeśli używasz platformy ASP.NET Core, następnie nie należy tego kodu od dostawcy bazy danych jest już skonfigurowana poza kontekstem (w pliku 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).

Dodaj Konstruktor do testowaniaAdd a constructor for testing

Najprostszym sposobem, aby umożliwić testowanie z inną bazą danych jest zmodyfikowanie kontekst ujawniać Konstruktor, który akceptuje 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)
    { }

Porada

DbContextOptions<TContext> informuje kontekstu, wszystkie jego ustawienia, takie jak której bazy danych, aby nawiązać połączenie.DbContextOptions<TContext> tells the context all of its settings, such as which database to connect to. Jest to ten sam obiekt, który jest wbudowana, uruchamiając metoda OnConfiguring w kontekście usługi.This is the same object that is built by running the OnConfiguring method in your context.

Pisanie testówWriting tests

Kluczem do testowania przy użyciu tego dostawcy jest możliwość opowiadania kontekst do użycia dostawcy InMemory i kontrolowanie zakresu bazy danych w pamięci.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. Zazwyczaj chcesz czyste bazy danych dla każdej metody testowej.Typically you want a clean database for each test method.

Oto przykład klasy testowej, korzystającej z bazy danych w pamięci.Here is an example of a test class that uses the InMemory database. Każdej metody testowej Określa unikatową nazwę bazy danych, co oznacza, że każda metoda charakteryzuje się własną bazę danych w pamięci.Each test method specifies a unique database name, meaning each method has its own InMemory database.

Porada

Aby użyć .UseInMemoryDatabase() metodę rozszerzenia, odwołanie do pakietu NuGet Microsoft.EntityFrameworkCore.InMemory.To use the .UseInMemoryDatabase() extension method, reference the NuGet package Microsoft.EntityFrameworkCore.InMemory.

using BusinessLogic;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace TestProject.InMemory
{
    [TestClass]
    public class BlogServiceTests
    {
        [TestMethod]
        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("http://sample.com");
            }

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

        [TestMethod]
        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 = "http://sample.com/cats" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/catfish" });
                context.Blogs.Add(new Blog { Url = "http://sample.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.AreEqual(2, result.Count());
            }
        }
    }
}