選單

資料庫測試的基礎要素

作者 | Jonathan Allen

譯者 | 屠靈

策劃 | 丁曉昀

眾所周知,在測試行業,模擬資料庫和其他持久化層會降低測試效率。在測試時,如果一個元件不屬於測試的一部分,就很難測試它與其他元件之間的互動行為。遺憾的是,這個行業只專注於功能層面的測試,很少有人接受過其他型別測試的培訓。這篇文章透過引入資料庫測試的概念來糾正這個問題。這些技術也適用於其他型別的持久化機制,比如呼叫微服務。

為了瞭解如何測試資料庫,我們先“忘記”與單元測試和整合測試相關的一些概念。直接一點說,現如今對這些術語的定義已經偏離了它們最初的含義。所以,在文章的剩餘部分,我們將不再使用它們。

測試最本質的目的是生成資訊。一個測試用例在執行完之後應該生成與被測試的東西相關的資訊,這些資訊是你原先不知道的。生成的資訊越多越好。因此,我們傾向於“一個測試用例應該儘可能提供可以證明某個事實所需的斷言”,而不是“一個測試用例只提供一個斷言”。

另一個有問題的觀點是“所有的測試都應該是獨立的”。人們通常會誤讀這個觀點,認為每一個測試都應該使用 Mock,你所測試的每一個功能應該與它們的依賴項隔離。但這樣是毫無意義的,因為在生產環境中,這些功能不可能與它們的依賴項隔離。相反,你應該儘可能像在生產環境中那樣測試,這樣才會發現儘可能多的問題。

“所有的測試都應該是獨立的”這句話真正的意思是說,每一個測試都可以獨立於其他測試執行。或者,換句話說,你可以按照任意的順序、在任意時刻執行每一個測試或一組測試。

很多人在測試時把事情弄複雜了。他們在執行每一個測試(甚至是每一個單獨的測試用例)之前都會完整地重建資料庫。這帶來了一些問題。

首先,測試變慢了。建立新資料庫和填充資料需要時間,這通常是造成資料庫測試變慢的直接原因,而這又反過來讓人們不願意去執行測試,甚至不準備這類測試。

另一個問題與資料庫裡的記錄數量有關。當資料庫裡只有一條程式碼,有些程式碼執行得很好,但當有成千上百條記錄時就會失敗。在某些情況下,比如查詢語句裡缺少了 WHERE 子句,只要兩條記錄就會導致測試失敗。

因此,我們需要編寫資料庫端的測試。不管在任何時候,你都應該用生產環境的資料副本來執行測試,並看著它們全部執行成功。

“。NET ORM Cookbook”就給出了一個很好的示例。這個專案有 1600 多個數據庫端測試用例,它們可以按照任意順序執行。為了理解其中的原理,我們將構建一些簡單的 CRUD 測試來解釋這些概念。

接下來的問題是一致性。人們常說,每一個測試都應該具備完美的一致性,也就是說,每次運行同一個測試都應該得到相同的結果。為了獲得一致性,不能使用基於時間或隨機生成的測試資料,也不能被環境影響到。

在測試資料庫時,這是無法實現的。因為總有一些不可預測的問題出現,比如網路連線問題、磁碟問題、舊資料,等等。

但並不是說不具備這種一致性的測試就是不可靠的。儘管一些屬性會不一致,但測試在大部分時間都會返回相同的結果。隨機出現的失敗讓可以你知道應用程式在哪些情況下會有怎樣的表現。

注意:本文所有的例子都可以再 GitHub 上找到。

建立記錄

我們的第一個測試是建立一條記錄。為了簡單起見,我們選擇了 EmployeeClassification 類,它只有四個欄位:

在檢查資料庫模式時,我們發現 EmployeeClassificationKey 是一個自生成數字欄位,所以就不用管它了。EmployeeClassificationName 有唯一性約束,這是給很多人造成麻煩的地方。

這個測試是不可重複執行的,因為在第二次執行它時,相同的名字已經存在了。為了解決這個問題,我們加了一個區分方式,比如時間戳或 GUID。

這個測試並沒有真正測試什麼東西。我們知道,CreateAsyn 沒有丟擲異常,但它可能是一個空方法。為了讓測試完整,我們需要加入讀操作。

建立和讀取記錄

在建立和讀取測試中,我們先確保可以從資料庫讀取到非 0 的鍵。然後,我們用這個鍵讀取記錄,並驗證從資料庫讀取的記錄欄位與原先的一樣。

注意:當沒有讀取到記錄時 Repository 並不會丟擲異常,所以,在屬性級別的斷言之前加入 Assert。IsNotNull,可以更好地捕獲測試失敗情況。

斷言太多會導致一些問題。首先,如果一個斷言失敗了,你不知道是哪一個。IsEmployee 和 IsExempt 都是 Boolean 型別,所以你都沒有辦法透過上下文資訊來判斷是哪個失敗了。你可以透過加入更多資訊來解決這個問題,如果測試框架支援的話。

其次,難以診斷。如果多個斷言失敗了,只有第一個被捕獲到,後續的資訊丟失了。為了解決這個問題,我們使用了 AssertionScope 物件。所有與之相關的斷言會被集中在一起,在 using 程式碼塊最後統一報出來。AssertionScope 的實現示例可以在 GitHub 上找到。對於更為複雜的建立,可以考慮使用流式 AssertionScope 或者 NUnit 的 Assert。Multiple。

隨著測試用例越來越多,這會變成一項枯燥的重複性工作,所以我們需要一個輔助方法。

你也可以不用手動寫這個方法,直接使用 CompareNETObjects 庫。

建立、更新和讀取記錄

接下來的測試我們要更新記錄,涉及一個建立操作和兩次讀取操作。

為了能夠知道為什麼比較操作會失敗,我們給 PropertiesAreEqual 方法加了一個 stepName 引數。

建立和刪除記錄

到目前為止,我們已經涵蓋了 CRUD 的 C、R 和 U,就差 D 了。在刪除測試中,我們仍然會讀取資料兩次。但是,我們會使用 Repository 另一個方法,當找不動記錄時返回 null。如果你的 Repository 沒有這個方法,請參考第 7 個示例。

如果你的資料庫使用了軟刪除,你還需要檢查相應的記錄是否更新了刪除標記。這可以透過以下幾行程式碼來實現。

建立記錄的改進

在第一個測試中,可選資料列總是使用預設值。這個可以透過資料驅動測試來解決。下面的例子針對的是 MSTest,不過其他主流的測試框架也有類似的東西。

現在,我們可以為單個測試建立多條記錄,需要具備檢視在資料庫建立了哪些記錄的能力。在 MSTest 中,我們可以使用 Debug。WriteLine 來記錄日誌。如果你用的是其他測試框架,可以參考它們的文件,找到相應的方法。

過濾記錄

到目前為止只涉及單條記錄,但一些 Repository 方法會返回多條記錄,這就帶來了一些額外的挑戰。

在接下來的測試中,我們要查詢 IsEmployee = true 和 IsExempt = false 的記錄。我們需要事先在資料庫中準備好匹配的記錄和不匹配的記錄。

我們需要兩種斷言。

斷言返回了我們事先插入的匹配的記錄。

斷言不返回非匹配的記錄。

注意第二種斷言。我們不僅僅要檢查我們新建立的非匹配記錄不會被返回,還要檢查其他非匹配的記錄,這涉及之前已存在的記錄。

我們不檢查記錄條數。除非你在測試中根據唯一值來過濾資料,否則,如果有其他測試也在使用同一個資料庫,就會有問題。這些問題通常出現在分片資料庫中,或者在並行執行測試用例時。

隨著時間推移,你會發現,返回的資料記錄條數會持續增加。當記錄條數的增加導致測試變慢,你需要考慮以下這些操作。

重置資料庫。

改進索引。

移除 Repository 的這些方法。

重置資料庫是最快的操作,但我通常很少會建議這麼做。儘管測試資料庫中會有很多記錄,但比起生產資料庫,仍然少很多個數量級。這意味著重置資料庫只會將效能問題隱藏掉。

改進索引有它自己的難點,因為每一個索引都會降低寫入效能。不過,如果你可以忍受,改進索引會給使用者帶來更好的體驗。

最後一個選項也需要考慮在內,特別是當這些方法返回很多資料。GetAll 方法只返回幾十條記錄是沒有問題的,但如果返回 1 萬條記錄,你就不應該考慮在生產環境中使用它,你應該將其移除。

關於清理

很多人建議在測試結尾把建立的記錄刪掉,甚至有人會把整個測試放在一個事務中,確保新建立的記錄會被刪掉。

一般來說,我並不鼓勵這種做法。測試資料庫通常不會有太多資料,回滾事務只會錯失累積資料的機會。

另外,清理操作有時候也會失敗,特別是當你手動刪除記錄而不是回滾事務時。這種脆弱的測試是我們要避免的。

說到事務,有人建議整個測試從頭到尾只使用一個事務。這可能是一種嚴重的反模式,它會影響你並行執行測試,因為它可能會阻塞資料庫(還可能出現死鎖)。況且,有些資料庫(比如 SQL Server)的回滾非常慢。

話雖如此,在測試中加入清理步驟並沒有錯,只是你要小心,不要讓測試時間變得太長或增加失敗情況。

結  論

持久化層的測試與類和方法的測試不一樣。這些技術不難掌握,與其他技術一樣,要掌握它們都需要練習。先從簡單的 CRUD 場景開始,再過渡到複雜的場景,比如並行測試、隨機取樣、效能測試和全資料集掃描。

作者簡介:

Jonathan Allen

在 90 年代後期開始為一家健康診所開發 MIS 專案,並逐步將它們從 Access 和 Excel 變成企業級解決方案。在花了五年時間為金融行業開發自動化交易系統之後,他成為了多個專案的顧問,包括機器人倉庫的 UI、癌症研究軟體的中間層,以及一家大型房地產保險公司的大資料需求。在業餘時間,他喜歡學習 16 世紀的武術,並撰寫相關的文章。

https://www。infoq。com/articles/Testing-With-Persistence-Layers/