想用單元測試專案單獨測試 ASP.NET Core 裡的 EF Core DbContext,一時間傻住不知如何下手,就知道又有基本功要練了。ASP.NET Core 重度依賴 DI,網站專案如要使用 EF Core DbContext 物件需在 Controller 或 Service 建構式新增 DbContext 型別的參數,DI 會在建立 Controller 或 Service 時傳入 DbContext Instance,但,在單元測試專案該怎麼做?

先來個題外話,.NET Core 的測試專案有三種選擇:MSTest、NUnit 及 xUnit:

(三種專案的比較可參考 MSTest, NUnit 3, xUnit.net 2.0 比較 by Yowko ,我只有 MSTest 經驗,選擇時反而沒有懸念,噗)

爬文找到兩種解法。在此借用 ASP.NET Core 新增修改刪除(CRUD)介面傻瓜範例 專案裡的 JournalDbContext 示範:

借用 Startup 註冊邏輯

ASP.NET Core 網站執行時,Program.cs 會執行 IWebHostBuilder.UseStartup<Startup>() 觸發 Startup.cs 的 ConfigureServices(IServiceCollection services),透過以下邏輯指定 EF Core 來源為 SQL Server 並傳入連線字串:

services.AddDbContextPool<JournalDbContext>(options =>
{
    //TODO: 實際應用時,連線字串應移入設定檔並加密儲存
    options.UseSqlServer(Configuration.GetConnectionString("LocalDB"));
});

依此原理,我們只需在單元測試專案裡也模擬 IWebHostBuilder.UseStartup<Startup>() 便能直接使用 Startup.cs 的資料庫設定。範例如下:

using CRUDExample;
using CRUDExample.Models;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace TestProject
{
    [TestClass]
    public class UnitTest1
    {
        static IWebHost _webHost = null;
        static T GetService<T>()
        {
            var scope = _webHost.Services.CreateScope();
            return scope.ServiceProvider.GetRequiredService<T>();
        }

        [ClassInitialize]
        public static void Init(TestContext testContext)
        {
            _webHost = WebHost.CreateDefaultBuilder()
                .UseStartup<Startup>()
                .Build();
        }

        [TestMethod]
        public void TestMethod1()
        {
            var ctx = GetService<JournalDbContext>();
            var rec = ctx.Records.FirstOrDefault();
            Assert.IsNotNull(rec);
        }
    }
}

測試成功!

自行建立 DbContext

有另一種情境,測試時期 EF Core 將使用不同的資料庫來源(例如平日測試連 SQL,單元測試則另開 LocalDB 資料庫),以免自動測試寫入亂七八糟資料影響日常開發,或是測試時直接改用可拋式的 InMemory 資料來源,測完即丟。

以下展示改用 InMemory 資料庫執行自動測試,EF Core 再次展現它跨資料庫的威力,由 UseSqlServer() 改為 UseInMemoryDatabase() 就變成用記憶體模擬資料庫,來去不留痕跡。

using CRUDExample.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestProject
{
    [TestClass]
    public class UnitTest2
    {
        JournalDbContext GetDbContext()
        {
            var options = new DbContextOptionsBuilder<JournalDbContext>()
                .UseInMemoryDatabase(databaseName: "TestDb")
                .Options;
            return new JournalDbContext(options);
        }

        [TestMethod]
        public void TestMethod2()
        {
            var ctx = GetDbContext();
            var data = new DailyRecord()
            {
                Date = DateTime.Now,
                EventSummary = "No Events",
                Remark = "ABC",
                Status = StatusFlags.Warn,
                User = "Jeffrey"
            };
            ctx.Records.Add(data);
            ctx.SaveChanges();
            var rec = ctx.Records.FirstOrDefault();
            Assert.IsNotNull(rec);
        }
    }
}

成功!

以上就是在單元測試使用 ASP.NET Core 內含 EF Core 程式的簡單示範。

Tips of how to access EF Core DbContext in ASP.NET Core project from unit test project.


Comments

# by 凱大

滿多人習慣慣用 real db 來作為測試 這其實並無法真的實現一個完整的測試 InMemory 完整了模組化所需要的東西 不過InMemory並不是模擬 Db 所以概念上對某些人而言可能反而並不那麼實用 (雖然我覺得這屬於他對於測試的不理解問題 XD) 相關MSDN: https://docs.microsoft.com/zh-tw/ef/core/miscellaneous/testing/in-memory

# by Jeffrey

to 凱大,雖然是 UnitTest 專案,有些人是用它跑涵蓋資料庫的自動化整合測試,也許不夠道地沒離TDD的精神也還很遠,但我認為有做到這一步已勝過完全不測試、手工測試或是上線玩全民公測,仍有成長空間但值得鼓勵。

Post a comment