在不使用生產資料庫系統的情況下進行測試

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

提示

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

存放庫模式

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

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

...以下是生產環境使用的部分範例實作:

public class BloggingRepository : IBloggingRepository
{
    private readonly BloggingContext _context;

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

    public Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

它沒有太多:存放庫只會包裝 EF Core 內容,並公開執行資料庫查詢和更新的方法。 要注意的重點是,我們的 GetAllBlogs 方法會傳 IEnumerable<Blog> 回 ,而不是 IQueryable<Blog> 。 傳回後者表示查詢運算子仍可透過結果組成,而要求 EF Core 仍參與轉譯查詢;這會在第一個位置取代存放庫的目的。 IEnumerable<Blog> 可讓我們輕鬆地存根或模擬存放庫傳回的內容。

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

services.AddScoped<IBloggingRepository, BloggingRepository>();

最後,我們的控制器會插入存放庫服務,而不是 EF Core 內容,並在其上執行方法:

private readonly IBloggingRepository _repository;

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

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(name);

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

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

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

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

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

您可以在這裡 檢視 完整的範例程式碼。

SQLite 記憶體中的 SQLite

SQLite 可以輕鬆地設定為測試套件的 EF Core 提供者,而不是生產資料庫系統(例如 SQL Server):如需詳細資訊, 請參閱 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 的原因)。 不過,請注意,記憶體內部資料庫會根目錄在內容的內部服務提供者中;雖然在大部分情況下,內容會共用相同的服務提供者,但使用不同的選項設定內容可能會觸發使用新的內部服務提供者。 如果是這種情況,請針對應該共用記憶體內部資料庫的所有內容,明確傳遞 相同的 實例 InMemoryDatabaseRootUseInMemoryDatabase (這通常是透過具有靜態 InMemoryDatabaseRoot 欄位來完成的)。

交易

請注意,根據預設,如果交易已啟動,記憶體內部提供者會擲回例外狀況,因為不支援交易。 您可能想要藉由將 EF Core 設定為忽略,以無訊息方式忽略 InMemoryEventId.TransactionIgnoredWarning 交易,如上述範例所示。 不過,如果您的程式碼實際上依賴交易式語意,例如取決於回復實際回復變更,您的測試將無法運作。

檢視

記憶體內部提供者允許透過 LINQ 查詢定義檢視,使用 ToInMemoryQuery

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