選擇測試策略

如概述概述中所述,您需要做出的基本決策是您的測試是否牽涉到生產資料庫系統,就像您的應用程式一樣,或您的測試是否會針對測試替身執行,這會取代您的生產資料庫系統。

針對實際的外部資源進行測試,而不是將它取代為測試替身,可能會涉及下列困難:

  1. 在許多情況下,對實際的外部資源進行測試是不可能或實用的。 例如,您的應用程式可能會與某些無法輕易測試的服務互動(因為速率限制,或缺少測試環境)。
  2. 即使可能牽涉到真正的外部資源,也可能非常緩慢:對雲端服務執行大量測試可能會導致測試花費太長的時間。 測試應該是開發人員日常工作流程的一部分,因此測試必須快速執行。
  3. 針對外部資源執行測試可能會涉及隔離問題,其中測試會互相干擾。 例如,針對資料庫平行執行的多個測試可能會修改數據,並導致彼此以各種方式失敗。 使用測試替身可以避免這種情況,因為每個測試都會針對自己的內存資源執行,因此自然地與其他測試隔離。

不過,針對測試替身通過的測試並不保證程式在實際外部資源上運行時能正常工作。 例如,資料庫測試替身可能會執行區分大小寫的字串比較,而生產資料庫系統執行不區分大小寫的比較。 只有在針對實際生產資料庫執行測試時,才會發現這類問題,使得這些測試成為任何測試策略的重要部分。

針對資料庫進行測試可能比看起來更容易

由於上述針對實際資料庫進行測試時發生困難,開發人員經常被建議首先使用測試替身,並擁有能夠經常在他們的機器上運行的強大測試套件;相比之下,涉及資料庫的測試應該降低執行頻率,而且在許多情況下涵蓋範圍也小得多。 我們建議對後者進行更多思考,並建議資料庫實際上受到上述問題的影響可能遠不如人們所想:

  1. 大部分的資料庫現在可以輕鬆地安裝在開發人員的計算機上。 像 Docker 這類容器式技術能讓這件事變得非常簡單,像 Testcontainers 這類函式庫則能協助自動化測試中容器化資料庫的生命週期。 像 GitHub WorkspacesDev Container 這類技術會幫你建立整個開發環境(包括資料庫)。 使用 SQL Server 時,也可以針對 Windows 上的 LocalDB 進行測試,或輕鬆地在 Linux 上設定 Docker 映像。
  2. 針對合理測試資料集的本地資料庫進行測試通常非常迅速:全部通訊都是在本地進行的,而且測試資料通常會在資料庫端的記憶體中緩衝。 EF Core 本身包含超過 30,000 項針對 SQL Server 的測試;這些測試可以在幾分鐘內可靠完成,並且在每次提交時藉由 CI 執行,且開發者也經常在本地執行。 一些開發人員轉向使用記憶體資料庫(即“虛擬”的資料庫),因為他們認為這樣可以提升速度,但這幾乎從來不是事實。
  3. 針對真實資料庫執行測試時,隔離確實是障礙,因為測試可能會修改數據並互相干擾。 不過,有各種技術可在資料庫測試案例中提供隔離,我們專注於在 生產資料庫系統上進行測試 時使用的這些技術。

上述並不是意圖貶損測試替身或反對它們的使用。 首先,某些情境無法在其他情況下進行測試,因此需要使用測試替身,例如模擬資料庫故障。 不過,在我們的經驗中,用戶經常因為上述原因而迴避對其資料庫進行測試,認為其速度緩慢、硬或不可靠,但情況不一定如此。 針對生產資料庫系統 進行測試旨在解決此問題,提供針對資料庫快速、隔離測試的指導方針和範例。

不同類型的測試替身

測試替身 是一個廣泛的術語,包含非常不同的方法。 本節涵蓋一些常見的技巧,使用測試替身以測試 EF Core 應用程式:

  1. 使用 SQLite (記憶體內部模式) 作為資料庫假的,取代您的生產資料庫系統。
  2. 使用EF Core 記憶體內部提供者做為假資料庫,取代您的生產資料庫系統。
  3. 模擬或存根 DbContextDbSet
  4. 引入 EF Core 與應用程式碼之間的儲存庫層,並對該層進行模擬或替代。

接下來,我們將探索每個方法的意義,並將其與其他方法進行比較。 建議您閱讀不同的方法,以充分瞭解每個方法。 如果您決定撰寫不涉及生產資料庫系統的測試,則存放庫層是唯一允許對數據層進行全面且可靠仿真的方法。 不過,這種方法在實作和維護方面具有顯著的成本。

SQLite 作為資料庫假

其中一個可能的測試方法是將生產資料庫(例如:SQL伺服器)替換為 SQLite,有效地將其用作測試模擬。 除了易於設定之外,SQLite 還有一項 記憶體內部資料庫 功能,特別適合用於測試:每個測試自然都會在自己的記憶體內部資料庫中隔離,而且不需要管理實際檔案。

不過,在執行此動作之前,請務必瞭解在 EF Core 中,不同的資料庫提供者的行為不同 - EF Core 不會嘗試抽象基礎資料庫系統的每個層面。 基本上,這表示針對 SQLite 進行測試並不保證與 SQL Server 或任何其他資料庫的結果相同。 以下是一些可能的行為差異範例:

  • 相同的 LINQ 查詢可能會在不同的提供者上傳回不同的結果。 例如,SQL Server 預設會執行不區分大小寫的字串比較,而 SQLite 則區分大小寫。 這可能讓測試在 SQLite 上通過,但在 SQL Server 上失敗(或反之亦然)。
  • 在 SQL Server 上運作的某些查詢不支援於 SQLite,因為這兩個資料庫的 SQL 支援有所不同。
  • 如果您的查詢碰巧使用提供者特定的方法,例如 SQL Server 的 EF.Functions.DateDiffDay,該查詢將會在 SQLite 上失敗,而且無法進行測試。
  • 原始 SQL 可能會運作,或可能會失敗或傳回不同的結果,視所執行的確切情況而定。 SQL 方言在資料庫中有許多方式不同。

相較於針對生產資料庫系統執行測試,開始使用 SQLite 相當容易,而且有許多用戶這樣做。 不幸的是,測試 EF Core 應用程式時,上述限制最終會變成問題,即使它們似乎不在一開始也一樣。 因此,我們建議您針對實際資料庫撰寫測試,或者如果使用測試替身是絕對必要的,請考慮以下所述儲存庫模式的成本。

如需如何使用 SQLite 進行測試的資訊, 請參閱本節

作為假資料庫的記憶體內部

作為 SQLite 的替代方案,EF Core 也隨附記憶體內部提供者。 雖然此提供者最初設計為支援 EF Core 本身的內部測試,但有些開發人員在測試 EF Core 應用程式時會將其當做資料庫假用。 強烈不建議這麼做:因為模擬資料庫與在記憶體中運行時有著與 SQLite 相同的問題(請參閱上述),但除此之外,還具有以下額外限制:

  • 記憶體內部提供者通常支援比 SQLite 提供者更少的查詢類型,因為它不是關係資料庫。 相較於生產資料庫,更多查詢將會失敗或行為不同。
  • 不支援交易。
  • 原始 SQL 完全不受支援。 您可以將它與 SQLite 進行比較,只要確保您的 SQL 在 SQLite 和生產環境的資料庫上能以相同的方式運行,就可以使用原始 SQL。
  • 記憶體內部提供者尚未針對效能進行優化,而且通常會比記憶體內部模式中的 SQLite 更慢(甚至您的生產資料庫系統)。

總而言之,記憶體具有 SQLite 的所有缺點,還有一些額外的缺點,而且沒有優點作為回報。 如果您要尋找簡單的內存資料庫仿真程序,請使用 SQLite,而不是內存提供者;但請考慮改用如下所示的儲存庫模式。

如需如何使用記憶體內部測試的資訊,請參閱 本節

模擬或虛設 DbContext 和 DbSet

此方法通常會使用模擬框架來創建DbContextDbSet的測試雙物件,並以這些雙物件作為測試的對象。 DbContext 模擬是測試各種 不涉及查詢的功能 的好方法,例如呼叫 AddSaveChanges(),可讓您驗證程式碼在寫入場景中呼叫它們。

不過,正確模擬DbSet查詢功能是不可行的,因為查詢是透過 LINQ 運算子來表示,這些是IQueryable上的靜態擴充方法呼叫。 因此,當有些人談論「模擬 DbSet」時,他們實際上是在談論建立一個由記憶體內部集合支援的 DbSet,然後在記憶體中對該集合執行查詢運算子,就像一個簡單的 IEnumerable。 這實際上不是模擬,而是一種假的,記憶體內部集合會取代實際資料庫。

由於只有 DbSet 本身是假的,而且查詢是在記憶體內部評估,因此此方法最終會與使用 EF Core 記憶體內部提供者非常類似:這兩種技術都會在 .NET 中透過記憶體內部集合執行查詢運算符。 因此,這項技術也有相同的缺點:查詢的行為會不同(例如區分大小寫),或只會失敗(例如,因為提供者特定的方法),原始 SQL 將無法運作,而且交易會忽略。 因此,通常應該避免這項技術來測試任何查詢程序代碼。

存放庫模式

上述方法嘗試將 EF Core 的生產資料庫提供者替換為虛擬測試提供者,或者建立一個由記憶體內部集合支援的 DbSet。 這些技術很類似,它們仍然會評估程式的LINQ查詢,無論是在SQLite或記憶體中,這最終都是上述困難的來源:針對特定生產資料庫執行的查詢無法在沒有問題的情況下可靠地在其他地方執行。

針對適當和可靠的測試替代物,請考慮引入一個在應用程式代碼與 EF Core 之間進行介化的儲存庫層。 存放庫的生產實作包含實際的LINQ查詢,並透過EF Core加以執行。 在測試中,存放庫抽象概念會直接進行擷取或模擬,而不需要任何實際的LINQ查詢,有效地從測試堆疊中移除EF Core,並允許測試單獨專注於應用程式程序代碼。

下圖將資料庫假方法 (SQLite/記憶體內部) 與存放庫模式進行比較:

假提供者與存放庫模式的比較

由於 LINQ 查詢已不再是測試的一部分,因此您可以直接將查詢結果提供給您的應用程式。 另一種方式是,上述方法大致允許擷 取查詢輸入 (例如,以記憶體內部數據表取代 SQL Server 數據表 ),但仍會執行記憶體中實際的查詢運算符。 相反地,存放庫模式可讓您直接截斷 查詢輸出 ,以便進行更強大且專注的單元測試。 請注意,若要讓此功能運作,您的存放庫無法公開任何 IQueryable 傳回方法,因為這些方法無法再次被擷取;應該改為傳回 IEnumerable。

不過,由於存放庫模式需要在IEnumerable-returning 方法中封裝每一個(可測試的)LINQ查詢,所以它會在您的應用程式上施加額外的架構層,而且可能會產生顯著的實作和維護成本。 在選擇如何測試應用程式時,不應該打折扣此成本,特別是考慮到對於存放庫所公開的查詢,仍可能需要針對實際資料庫進行測試。

值得注意的是,存放庫在測試之外確實具有優勢。 它們可確保所有數據存取程序代碼都集中在一個位置,而不是分散在應用程式周圍,而且如果您的應用程式需要支援多個資料庫,則存放庫抽象概念對於調整跨提供者的查詢很有説明。

如需顯示使用存放庫測試的範例, 請參閱本節

整體比較

下表提供不同測試技術快速、比較的檢視,並顯示哪些功能可在哪一種方法下進行測試:

功能 記憶體中 SQLite 記憶體中的 SQLite 模擬 DbContext 存放庫模式 針對資料庫進行測試
測試替身類型 模擬/存根 真實,無重複
原始 SQL? No 取決於 No .是 .是
交易? 否(忽略) .是 .是 .是 .是
針對特定提供者的翻譯? No No No .是 .是
確切的查詢行為? 視情況而定 取決於 取決於 .是 .是
可以在應用程式中的任何位置使用 LINQ 嗎? .是 .是 .是 不* .是

* 所有可測試的資料庫 LINQ 查詢都必須封裝在返回 IEnumerable 的儲存庫方法中,才能進行存根/模擬。

摘要

  • 我們建議開發人員對其真實生產環境資料庫系統執行的應用程式有良好的測試涵蓋範圍。 這可讓您確信應用程式實際上可在生產環境中運作,且設計正確,測試可以可靠地且快速地執行。 由於在任何情況下都需要這些測試,因此最好從該處開始,如有需要,請視需要使用測試雙倍新增測試。
  • 如果您決定使用測試替代物,建議您實現儲存庫模式,這樣您可以針對 EF Core 執行存根 (stub) 或模擬 (mock) 的資料存取層,而不是使用虛假的 EF Core 提供者(Sqlite/記憶體內部)或模擬 DbSet
  • 如果因為某些原因,存放庫模式不是可行的選項,請考慮使用 SQLite 記憶體內部資料庫。
  • 請避免記憶體內部提供者進行測試, 不建議這樣做,而且只支援舊版應用程式。
  • 避免針對查詢目的進行模擬 DbSet