不使用生產資料庫系統進行測試
在此頁面中,我們會討論撰寫自動化測試的技術,這些測試不會牽涉到應用程式在生產環境中執行的資料庫系統,方法是將資料庫與 測試雙重交換。 選擇測試策略時,有各種不同類型的測試替身和方法。建議您徹底閱讀 選擇測試策略,以充分瞭解不同的選項。 最後,您也可以針對您的生產資料庫系統進行測試,如在 測試生產資料庫系統所述。
存放庫模式
如果您已決定撰寫測試而不涉及生產資料庫系統,則建議的作法是存放庫模式;如需詳細資訊的背景,請參閱本節 。 實作存放庫模式的第一個步驟是將 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 }));