共用方式為


不使用生產資料庫系統進行測試

在此頁面中,我們會討論撰寫自動化測試的技術,這些測試不會牽涉到應用程式在生產環境中執行的資料庫系統,方法是將資料庫與 測試雙重交換。 選擇測試策略時,有各種不同類型的測試替身和方法。建議您徹底閱讀 選擇測試策略,以充分瞭解不同的選項。 最後,您也可以針對您的生產資料庫系統進行測試,如在 測試生產資料庫系統所述。

提示

此頁面會顯示 xUnit 技術,但其他測試架構中也有類似的概念,包括 NUnit

存放庫模式

如果您已決定撰寫測試而不涉及生產資料庫系統,則建議的作法是存放庫模式;如需詳細資訊的背景,請參閱本節 。 實作存放庫模式的第一個步驟是將 EF Core LINQ 查詢擷取到個別層,稍後我們會將其存根或模擬。 以下是部落格系統的存放庫介面範例:

public interface IBloggingRepository
{
    Task<Blog> GetBlogByNameAsync(string name);

    IAsyncEnumerable<Blog> GetAllBlogsAsync();

    void AddBlog(Blog blog);

    Task SaveChangesAsync();
}

...以下是用於生產環境的實作範例:

public class BloggingRepository : IBloggingRepository
{
    private readonly BloggingContext _context;

    public BloggingRepository(BloggingContext context)
        => _context = context;

    public async Task<Blog> GetBlogByNameAsync(string name)
        => await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);

    // Other code...
}

這其實沒什麼特別:儲存庫僅僅是一個封裝了 EF Core 上下文的介面,並提供執行資料庫查詢與更新的方法。 要注意的重點是,我們的 GetAllBlogs 方法會傳回 IAsyncEnumerable<Blog>(或 IEnumerable<Blog>),而不是 IQueryable<Blog>。 選擇傳回後者意味著查詢運算符仍然可以對結果進行構建,進而要求 EF Core 仍參與轉譯查詢;這將使建立存放庫的初衷失去意義。 IAsyncEnumerable<Blog> 可讓我們輕鬆地存根或模擬存放庫傳回的內容。

針對 ASP.NET Core 應用程式,我們需要將存放庫註冊為相依性注入中的服務,為此應將下列內容新增至應用程式的 ConfigureServices

services.AddScoped<IBloggingRepository, BloggingRepository>();

最後,我們的控制器會注入儲存庫服務,而不是 EF Core 上下文,並在其上執行方法:

private readonly IBloggingRepository _repository;

public BloggingControllerWithRepository(IBloggingRepository repository)
    => _repository = repository;

[HttpGet]
public async Task<Blog> GetBlog(string name)
    => await _repository.GetBlogByNameAsync(name);

此時,您的應用程式會根據存放庫模式來建構:與數據存取層 - EF Core 的唯一接觸點現在會透過存放庫層,在應用程式程式代碼與實際資料庫查詢之間做為調解器。 測試現在只要擷取存放庫,或用您慣用的模擬連結庫進行模擬,即可撰寫測試。 以下是使用熱門 Moq 函式庫的模擬型測試範例:

[Fact]
public async Task GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByNameAsync("Blog2"))
        .Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

    // Act
    var blog = await controller.GetBlog("Blog2");

    // Assert
    repositoryMock.Verify(r => r.GetBlogByNameAsync("Blog2"));
    Assert.Equal("http://blog2.com", blog.Url);
}

您可以在這裏檢視完整的範例程式代碼

SQLite 記憶體中

SQLite 可以輕鬆地設定為測試套件的 EF Core 提供者,而不是生產資料庫系統(e.g. SQL伺服器):如需詳細資訊,請參閱 SQLite 提供者檔。 不過,在測試時,通常最好使用 SQLite 的 記憶體內部資料庫 功能,因為它可在測試之間提供簡單的隔離,而且不需要處理實際的 SQLite 檔案。

若要使用內存 SQLite 資料庫,請務必瞭解每當開啟低層連接時就會建立新的資料庫,並在該連接關閉時即會被刪除。 在一般使用情況下,EF Core 的 DbContext 會在每次執行查詢時視需要開啟並關閉資料庫連線,以避免將連線保留不必要的長時間。 不過,使用記憶體內部的 SQLite 時,這將導致每次都重設資料庫。因此,作為解決方案,我們在將連接傳遞給 EF Core 之前先開啟它,並安排只在測試完成時再關閉。

    public SqliteInMemoryBloggingControllerTest()
    {
        // Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
        // at the end of the test (see Dispose below).
        _connection = new SqliteConnection("Filename=:memory:");
        _connection.Open();

        // These options will be used by the context instances in this test suite, including the connection opened above.
        _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlite(_connection)
            .Options;

        // Create the schema and seed some data
        using var context = new BloggingContext(_contextOptions);

        if (context.Database.EnsureCreated())
        {
            using var viewCommand = context.Database.GetDbConnection().CreateCommand();
            viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
            viewCommand.ExecuteNonQuery();
        }

        context.AddRange(
            new Blog { Name = "Blog1", Url = "http://blog1.com" },
            new Blog { Name = "Blog2", Url = "http://blog2.com" });
        context.SaveChanges();
    }

    BloggingContext CreateContext() => new BloggingContext(_contextOptions);

    public void Dispose() => _connection.Dispose();

測試現在可以呼叫 CreateContext,它會使用我們在建構函式中設定的連接傳回環境,以確保我們有一個帶有預載數據的全新資料庫。

您可以在這裡查看 SQLite 記憶體中測試的範例程式碼,這裡

內存提供者

測試概觀頁面所述,強烈建議不要使用記憶體內部提供者進行測試:請考慮改用 SQLite,或 實作存放庫模式。 如果您決定使用記憶體內部,以下是一般測試類別建構函式,會在每次測試之前,先設定並植入新的記憶體內部資料庫:

public InMemoryBloggingControllerTest()
{
    _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
        .UseInMemoryDatabase("BloggingControllerTest")
        .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
        .Options;

    using var context = new BloggingContext(_contextOptions);

    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    context.AddRange(
        new Blog { Name = "Blog1", Url = "http://blog1.com" },
        new Blog { Name = "Blog2", Url = "http://blog2.com" });

    context.SaveChanges();
}

您可以在這裏檢視記憶體內測試的完整範例程式代碼

記憶體內部資料庫命名

記憶體內部資料庫是透過簡單的字串名稱來識別,而且可以藉由提供相同的名稱來連線到相同的資料庫數次(這就是為什麼上述範例必須在每次測試之前呼叫 EnsureDeleted)。 不過,請注意,記憶體資料庫是基於內容的內部服務提供者運作;雖然在大多數情況下,內容會共用相同的服務提供者,但使用不同的選項設定內容可能會導致使用新的內部服務提供者。 如果是這種情況,請明確傳遞相同的 InMemoryDatabaseRoot 實例給所有應該共用記憶體內資料庫的上下文 UseInMemoryDatabase(這通常是透過具有靜態 InMemoryDatabaseRoot 欄位來完成)。

交易

請注意,根據預設,如果交易已啟動,記憶體內部提供者會擲回例外狀況,因為不支援交易。 您可能希望透過將 EF Core 配置為忽略 InMemoryEventId.TransactionIgnoredWarning(如上例所示),達到讓交易被靜默忽略的效果。 不過,如果您的程式代碼實際上依賴交易式語意,例如取決於回復實際回復變更,您的測試將無法運作。

視圖

內存提供者允許透過 LINQ 查詢來定義視圖,使用 ToInMemoryQuery

modelBuilder.Entity<UrlResource>()
    .ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));