使用 SQLite 進行測試Testing with SQLite

SQLite 具有記憶體中模式,可讓您使用 SQLite 撰寫測試針對關聯式資料庫,而不實際的資料庫作業的額外負荷。SQLite has an in-memory mode that allows you to use SQLite to write tests against a relational database, without the overhead of actual database operations.

提示

您可以檢視這篇文章範例GitHub 上You can view this article's sample on GitHub

範例測試案例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. 它會有用交換此內容,連接到記憶體中的 SQLite 資料庫,以便我們可以撰寫有效率的測試,這項服務,而不需要修改程式碼,或執行許多工作,以建立測試 double 的內容。It would be useful to swap this context to connect to an in-memory SQLite 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.

提示

如果您使用 ASP.NET Core,然後您應該不需要此程式碼由於資料庫提供者設定 (在 Startup.cs) 的內容之外。If you are using ASP.NET Core, then you should not need this code since your database provider is configured outside of the context (in Startup.cs).

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

新增測試的建構函式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

要測試與此提供者的索引鍵是能夠告訴地使用 SQLite,及控制記憶體中資料庫範圍的內容。The key to testing with this provider is the ability to tell the context to use SQLite, and control the scope of the in-memory database. 資料庫的範圍會受到開啟及關閉連接。The scope of the database is controlled by opening and closing the connection. 資料庫範圍的連接已開啟的持續時間。The database is scoped to the duration that the connection is open. 通常您會想乾淨的資料庫的每個測試方法。Typically you want a clean database for each test method.

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

namespace TestProject.SQLite
{
    [TestClass]
    public class BlogServiceTests
    {
        [TestMethod]
        public void Add_writes_to_database()
        {
            // In-memory database only exists while the connection is open
            var connection = new SqliteConnection("DataSource=:memory:");
            connection.Open();

            try
            {
                var options = new DbContextOptionsBuilder<BloggingContext>()
                    .UseSqlite(connection)
                    .Options;

                // Create the schema in the database
                using (var context = new BloggingContext(options))
                {
                    context.Database.EnsureCreated();
                }

                // 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);
                }
            }
            finally
            {
                connection.Close();
            }
        }

        [TestMethod]
        public void Find_searches_url()
        {
            // In-memory database only exists while the connection is open
            var connection = new SqliteConnection("DataSource=:memory:");
            connection.Open();

            try
            {
                var options = new DbContextOptionsBuilder<BloggingContext>()
                    .UseSqlite(connection)
                    .Options;

                // Create the schema in the database
                using (var context = new BloggingContext(options))
                {
                    context.Database.EnsureCreated();
                }

                // 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());
                }
            }
            finally
            {
                connection.Close();
            }
        }
    }
}