使用 InMemory 測試Testing with InMemory

InMemory 提供者時,您想要測試元件使用的近似於連接至實際的資料庫,而不實際的資料庫作業的額外負荷。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.

提示

您可以在 GitHub 上檢視此文章的範例 (英文)。You can view this article's sample on GitHub.

InMemory 不是關聯式資料庫InMemory is not a relational database

EF 核心資料庫提供者可能沒有關聯式資料庫。EF Core database providers do not have to be relational databases. InMemory 的設計是為了測試,一般用途的資料庫,而且不是模擬的關聯式資料庫。InMemory is designed to be a general purpose database for testing, and is not designed to mimic a relational database.

包括的一些範例:Some examples of this include:

  • InMemory 可讓您儲存違反參考完整性條件約束,關聯式資料庫中的資料。InMemory will allow you to save data that would violate referential integrity constraints in a relational database.

  • 如果您使用 DefaultValueSql(string) 屬性在模型中,這是關聯式資料庫 API 並不會影響對 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.

提示

針對許多測試目的將不重要了這些差異。For many test purposes these differences will not matter. 不過,如果您想要測試的行為與類似,則為 true 的關聯式資料庫,請考慮使用SQLite 記憶體中模式However, if you want to test against something that behaves more like a true relational database, then consider using SQLite in-memory mode.

範例測試案例Example testing scenario

請考慮下列服務,可讓應用程式程式碼來執行一些部落格與相關的作業。Consider the following service that allows application code to perform some operations related to blogs. 它會在內部使用DbContext連線至 SQL Server 資料庫。Internally it uses a DbContext that connects to a SQL Server database. 它可能有助於交換此內容,讓我們可以撰寫有效率測試此服務而不必修改程式碼,或執行許多工作,建立測試連接到 InMemory 資料庫內容的 double。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();
        }
    }
}

準備您的內容Get your context ready

避免設定兩個資料庫提供者Avoid configuring two database providers

在測試中您要從外部設定要使用的 InMemory 提供者的內容。In your tests you are going to externally configure the context to use the InMemory provider. 如果您要設定資料庫提供者藉由覆寫OnConfiguring您的內容,則您需要加入一些條件式的程式碼,以確保您只設定資料庫提供者,如果其中一個已尚未設定。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");
    }
}

提示

如果您使用 ASP.NET Core,然後您應該不需要此程式碼由於外部 (在 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).

加入測試的建構函式Add a constructor for testing

若要啟用測試不同的資料庫最簡單的方式是修改內容公開 (expose) 的建構函式可接受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)
    { }

提示

DbContextOptions<TContext>告知其設定,例如連接到哪一個資料庫的所有內容。DbContextOptions<TContext> tells the context all of its settings, such as which database to connect to. 這是建立在您的內容中執行 OnConfiguring 方法相同的物件。This is the same object that is built by running the OnConfiguring method in your context.

撰寫測試Writing tests

要測試與此提供者的索引鍵是能夠判斷要使用的 InMemory 提供者,並控制記憶體中資料庫範圍的內容。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. 您通常想全新資料庫的每個測試方法。Typically you want a clean database for each test method.

以下是使用 InMemory 資料庫測試類別的範例。Here is an example of a test class that uses the InMemory database. 每個測試方法指定唯一的資料庫名稱,這表示每一種方法有它自己的 InMemory 資料庫。Each test method specifies a unique database name, meaning each method has its own InMemory database.

提示

若要使用.UseInMemoryDatabase()擴充方法,參考的 NuGet 套件Microsoft.EntityFrameworkCore.InMemoryTo 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());
            }
        }
    }
}