使用 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 Core 資料庫提供者可能沒有關聯式資料庫。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.
  • 透過時間戳記/資料列版本的並行存取([Timestamp]IsRowVersion) 不支援。Concurrency via Timestamp/row version ([Timestamp] or IsRowVersion) is not supported. DbUpdateConcurrencyException如果完成的更新將會擲回使用舊的並行語彙基元。No DbUpdateConcurrencyException will be thrown if an update is done using an old concurrency token.

提示

有許多測試用途的這些差異會造成影響。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());
            }
        }
    }
}