針對生產資料庫系統進行測試
在此頁面中,我們會討論撰寫自動化測試的技術,這些測試牽涉到應用程式在生產環境中執行的資料庫系統。 替代測試方法存在,其中生產資料庫系統會由測試雙精度浮點數交換;如需詳細資訊, 請參閱測試概觀頁面 。 請注意,測試與生產環境中使用的資料庫不同(例如 Sqlite)並未在此涵蓋,因為不同的資料庫會作為測試雙精度浮點數;此方法涵蓋在 測試中,而不需要生產資料庫系統 。
涉及實際資料庫之測試的主要障礙是確保適當的測試隔離,讓平行執行的測試(甚至序列中)不會互相干擾。 您可以在這裡 檢視 下列的完整範例程式碼。
設定資料庫系統
現在大部分的資料庫系統都可以在 CI 環境和開發人員電腦上輕鬆安裝。 雖然透過一般安裝機制安裝資料庫通常很容易,但現成可用的 Docker 映射適用于大多數主要資料庫,而且可在 CI 中特別容易安裝。 針對開發人員環境 GitHub 工作區 , 開發容器 可以設定所有必要的服務和相依性,包括資料庫。 雖然這需要對設定進行初始投資,但一旦完成,您就擁有工作測試環境,而且可以專注于更重要的事情。
在某些情況下,資料庫會有特殊版本或版本,有助於測試。 使用 SQL Server 時, LocalDB 可以用來在本機執行測試,幾乎完全沒有設定,視需要啟動資料庫實例,並可能節省較不強大的開發人員機器上的資源。 不過,LocalDB 沒有問題:
- 它無法提供 SQL Server Developer Edition 的所有支援。
- 它只能在 Windows 上使用。
- 啟動服務時,可能會造成第一個測試回合延遲。
我們通常建議安裝 SQL Server Developer 版本,而不是 LocalDB,因為它提供完整的 SQL Server 功能集,而且通常很容易做到。
使用雲端資料庫時,通常適合針對資料庫的本機版本進行測試,以提升速度和降低成本。 例如,在生產環境中使用 SQL Azure 時,您可以針對本機安裝的 SQL Server 進行測試 - 這兩者非常類似(雖然在進入生產環境之前,對 SQL Azure 本身執行測試仍然明智)。 使用 Azure Cosmos DB 時, Azure Cosmos DB 模擬器 是本機開發及執行測試的公用程式。
建立、植入和管理測試資料庫
安裝資料庫之後,您就可以開始在測試中使用資料庫。 在大部分的簡單案例中,您的測試套件有單一資料庫,可在多個測試類別的多個測試之間共用,因此我們需要一些邏輯,以確保在測試回合的存留期內,建立並植入資料庫一次。
使用 Xunit 時,這可以透過 類別裝置來完成,此裝置 代表資料庫,而且會在多個測試回合之間共用:
public class TestDatabaseFixture
{
private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTestSample;Trusted_Connection=True;ConnectRetryCount=0";
private static readonly object _lock = new();
private static bool _databaseInitialized;
public TestDatabaseFixture()
{
lock (_lock)
{
if (!_databaseInitialized)
{
using (var context = CreateContext())
{
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();
}
_databaseInitialized = true;
}
}
}
public BloggingContext CreateContext()
=> new BloggingContext(
new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(ConnectionString)
.Options);
}
當上述裝置具現化時,它會使用 EnsureDeleted() 卸載資料庫(以防先前執行存在),然後使用 EnsureCreated() 最新的模型組態建立它( 請參閱這些 API 的檔)。 建立資料庫之後,裝置會植入一些測試可以使用的資料。 值得花一些時間思考種子資料,因為稍後變更新的測試可能會導致現有的測試失敗。
若要在測試類別中使用裝置,只要透過您的裝置類型實 IClassFixture
作,xUnit 就會將它插入您的建構函式:
public class BloggingControllerTest : IClassFixture<TestDatabaseFixture>
{
public BloggingControllerTest(TestDatabaseFixture fixture)
=> Fixture = fixture;
public TestDatabaseFixture Fixture { get; }
您的測試類別現在具有 Fixture
屬性,可供測試用來建立功能完整的內容實例:
[Fact]
public void GetBlog()
{
using var context = Fixture.CreateContext();
var controller = new BloggingController(context);
var blog = controller.GetBlog("Blog2").Value;
Assert.Equal("http://blog2.com", blog.Url);
}
最後,您可能已經注意到上述裝置的建立邏輯中有一些鎖定。 如果裝置只用于單一測試類別,則保證由 xUnit 完全具現化一次;但在多個測試類別中,通常會使用相同的資料庫裝置。 xUnit 確實提供 收集裝置 ,但該機制會防止測試類別平行執行,這對測試效能很重要。 若要使用 xUnit 類別裝置安全地管理此功能,我們會對資料庫建立和植入採取簡單的鎖定,並使用靜態旗標來確保我們永遠不需要執行兩次。
測試修改資料
上述範例顯示唯讀測試,這是測試隔離觀點的簡單案例:因為沒有任何修改,因此無法測試干擾。 相反地,測試哪些修改資料比較有問題,因為它們可能會互相干擾。 隔離撰寫測試的一個常見技巧是將測試包裝在交易中,並在測試結束時復原該交易。 由於實際上不會認可任何資料庫,因此不會避免其他測試看到任何修改和干擾。
以下是將部落格新增至資料庫的控制器方法:
[HttpPost]
public ActionResult AddBlog(string name, string url)
{
_context.Blogs.Add(new Blog { Name = name, Url = url });
_context.SaveChanges();
return Ok();
}
我們可以使用下列專案來測試此方法:
[Fact]
public void AddBlog()
{
using var context = Fixture.CreateContext();
context.Database.BeginTransaction();
var controller = new BloggingController(context);
controller.AddBlog("Blog3", "http://blog3.com");
context.ChangeTracker.Clear();
var blog = context.Blogs.Single(b => b.Name == "Blog3");
Assert.Equal("http://blog3.com", blog.Url);
}
上述測試程式碼的一些注意事項:
- 我們會開始交易,以確保下列變更未認可至資料庫,且不會干擾其他測試。 由於永遠不會認可交易,所以在處置內容實例時,會在測試結束時隱含回復。
- 進行我們想要的更新之後,我們會使用 清除內容實例的變更追蹤器 ChangeTracker.Clear ,以確保我們確實從下列資料庫載入部落格。 我們可以改用兩個內容實例,但我們必須確定這兩個實例使用相同的交易。
- 您甚至可以想要在裝置
CreateContext
的 中啟動交易,讓測試接收已經在交易中的內容實例,並準備好進行更新。 這有助於防止意外忘記交易的情況,導致測試干擾,而可能難以偵錯。 您也可以在不同的測試類別中分隔唯讀和寫入測試。
測試如何明確管理交易
有一個最終的測試類別,其中呈現額外的困難:測試會修改資料,同時明確管理交易。 因為資料庫通常不支援巢狀交易,所以無法如上所示使用交易進行隔離,因為它們需要由實際的產品名稱使用。 雖然這些測試通常比較罕見,但必須以特殊方式處理它們:您必須在每次測試之後清除資料庫的原始狀態,而且必須停用平行處理,讓這些測試不會互相干擾。
讓我們檢查下列控制器方法作為範例:
[HttpPost]
public ActionResult UpdateBlogUrl(string name, string url)
{
// Note: it isn't usually necessary to start a transaction for updating. This is done here for illustration purposes only.
using var transaction = _context.Database.BeginTransaction(IsolationLevel.Serializable);
var blog = _context.Blogs.FirstOrDefault(b => b.Name == name);
if (blog is null)
{
return NotFound();
}
blog.Url = url;
_context.SaveChanges();
transaction.Commit();
return Ok();
}
假設基於某些原因,此方法需要使用可序列化的交易(這通常不是這種情況)。 因此,我們無法使用交易來保證測試隔離。 由於測試實際上會將變更認可至資料庫,因此我們會使用自己的個別資料庫來定義另一個裝置,以確保我們不會干擾上面已顯示的其他測試:
public class TransactionalTestDatabaseFixture
{
private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTransactionalTestSample;Trusted_Connection=True;ConnectRetryCount=0";
public BloggingContext CreateContext()
=> new BloggingContext(
new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(ConnectionString)
.Options);
public TransactionalTestDatabaseFixture()
{
using var context = CreateContext();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
Cleanup();
}
public void Cleanup()
{
using var context = CreateContext();
context.Blogs.RemoveRange(context.Blogs);
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
}
此裝置與上述裝置類似,但值得注意的是包含 Cleanup
方法;我們會在每次測試之後呼叫此裝置,以確保資料庫重設為其起始狀態。
如果這個裝置只供單一測試類別使用,我們可以將它當做上述類別裝置來參考 -xUnit 不會在同一類別內平行處理測試(深入瞭解 xUnit 檔中 的測試集合和平行處理 )。 不過,如果我們想要在多個類別之間共用此裝置,我們必須確定這些類別不會平行執行,以避免任何干擾。 若要這樣做,我們將使用此裝置作為 xUnit 集合裝置 ,而不是作為 類別裝置 。
首先,我們會定義 測試集合,該集合 會參考我們的裝置,並將供所有需要它的交易式測試類別使用:
[CollectionDefinition("TransactionalTests")]
public class TransactionalTestsCollection : ICollectionFixture<TransactionalTestDatabaseFixture>
{
}
我們現在參考測試類別中的測試集合,並接受建構函式中的裝置,如下所示:
[Collection("TransactionalTests")]
public class TransactionalBloggingControllerTest : IDisposable
{
public TransactionalBloggingControllerTest(TransactionalTestDatabaseFixture fixture)
=> Fixture = fixture;
public TransactionalTestDatabaseFixture Fixture { get; }
最後,我們會讓測試類別可處置,並安排在每次測試之後呼叫裝置 Cleanup
的方法:
public void Dispose()
=> Fixture.Cleanup();
請注意,由於 xUnit 只會具現化集合裝置一次,因此我們不需要像上述那樣使用鎖定資料庫建立和植入。
您可以在這裡 檢視 上述的完整範例程式碼。
提示
如果您有多個測試類別與修改資料庫的測試,您仍然可以使用不同的裝置平行執行它們,每個都參考自己的資料庫。 建立及使用許多測試資料庫並無問題,而且應該在一切有説明時完成。
有效率的資料庫建立
在上述範例中,我們使用 EnsureDeleted() 和執行 EnsureCreated() 測試之前,確定我們有最新的測試資料庫。 某些資料庫中的這些作業可能會有點慢,當您逐一查看程式碼變更並重複執行測試時,可能會發生問題。 如果是這種情況,您可能會想要在裝置的建構函式中暫時批註化 EnsureDeleted
:這會在測試回合之間重複使用相同的資料庫。
這種方法的缺點是,如果您變更 EF Core 模型,您的資料庫架構將不會是最新的,而且測試可能會失敗。 因此,我們只建議您在開發週期期間暫時執行這項操作。
有效率的資料庫清除
我們上面看到,當變更實際認可至資料庫時,我們必須在每個測試之間清除資料庫,以避免干擾。 在上述交易測試範例中,我們使用 EF Core API 來刪除資料表的內容來執行這項作業:
using var context = CreateContext();
context.Blogs.RemoveRange(context.Blogs);
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
這通常不是清除資料表最有效率的方式。 如果測試速度是個問題,您可能想要改用未經處理的 SQL 來刪除資料表:
DELETE FROM [Blogs];
您也可以考慮使用 重新寄出 套件,以有效率地清除資料庫。 此外,您不需要指定要清除的資料表,因此不需要更新清除程式碼,因為資料表會新增至模型。
摘要
- 針對實際資料庫進行測試時,值得區分下列測試類別:
- 唯讀測試相當簡單,而且一律可以針對相同的資料庫平行執行,而不必擔心隔離。
- 寫入測試比較有問題,但交易可用來確定它們已正確隔離。
- 交易式測試最有問題,需要邏輯將資料庫重設為其原始狀態,以及停用平行處理。
- 將這些測試類別分成不同的類別,可能會避免測試之間的混淆和意外干擾。
- 請為植入的測試資料提供一些預先思考,並嘗試以在種子資料變更時不會太常中斷的方式撰寫測試。
- 使用多個資料庫平行處理修改資料庫的測試,也可能允許不同的種子資料組態。
- 如果測試速度是個問題,您可能想要查看建立測試資料庫的更有效率的技術,以及在執行之間清除其資料。
- 請務必記住測試平行處理和隔離。