資料庫測試的重要性——永遠不要忘記資料庫測試

高翌翔發表於2012-01-04

原文連結:The Importance of Database Testing

特別說明:歡迎大家積極參與【iTran樂譯】第2期活動!

譯者導讀:本文分為三部分,第一部分是第1節,即說明“對資料庫測試的根本誤解”;第二部分從第2節至倒數第4節,詳述“資料庫測試測什麼”的問題;第三部分是最後3節,引出“資料庫測試怎麼測”的問題,提出自動部署、自動化測試、持續整合的思路及工具。另,副標題是俺自行添上去的,以明示本文意圖。


對資料庫測試的根本誤解

有許多關於測試驅動開發(Test-Driven Development,縮寫為TDD)的書籍。那些書通常關注的是將測試應用於工作單元(units of work)。對於工作單元的理解有許多種不同的方式,通常它表示一個類(class)。正如那些書中所言:編寫許多測試,以使那些測試都能通過的方式建立程式碼。應模擬所有的外部資源,以便你可以只測試這個單元。

這很酷,但不幸的是所有的測試在此刻停止了。因為通常會有些沒被測到的查詢(手寫的或者是由某些ORM工具生成的)。有些程式設計師使用整合測試來測試那些查詢——連線到一個真實的資料庫並執行真實的查詢來進行測試。這種做法通常意味著在測試快樂路徑(happy path)——我已經有了ORM工具,所以它會搞定每件事,我無須費心。

資料庫通常是一家公司最有價值的資產。應用程式可以一遍一遍重寫。舊的應用程式扔出去,新的應用程式裝進來。但是更換應用程式時沒人會丟棄滿載資料的資料庫。而是將資料庫小心翼翼地遷移過去。位於多個系統中的許多不同的應用程式會在同一時刻使用同一資料庫。這就是為什麼擁有充滿約束的良好資料庫模型是如此重要、以及為什麼應謹慎對待資料庫的原因。你真的不想破壞資料一致性(consistency),因為這會使你的公司付出高昂的代價。

本文是關於經常被遺忘的資料庫測試的。使用真實資料進行整合測試。實際上,它與你所使用的資料庫引擎的型別無關緊要。你可以使用PostgreSQLMySQLOracle,或者甚至使用那些有趣的noSQL資料庫,例如MongoDB。以下規則可適用於各種資料庫和各類應用程式。也許不是全部,例如noSQL資料庫就無法強制實施資料完整性(integrity)。

你的應用程式通常是由許多不同的部件組成的。其中有一些<將任何你喜歡的語言放在這裡>程式碼,一些配置檔案,一些SQL查詢,一些外部系統。測試一個應用程式意味著分別測試每個部件(因為只有這樣才更容易找出bug)、以及測試所有部件是如何協作的。資料庫就是這些部件的其中之一,而且你應該徹底測試它。

不測試資料庫

這是首要的、最可怕的錯誤。根本不測試資料庫。你編寫了一些使用資料庫的程式碼。你甚至使用一些模擬資料庫連線為這些類建立了單元測試。

整合測試怎麼樣?整合測試應在生產環境下對應用程式進行測試。整合測試背後的唯一想法是,確保應用程式部署到生產環境後可以正常工作。如果你不在生產資料庫上測試應用程式,那麼你實際上不併不知道應用程式能否工作。你的模擬連線讓你可以傳送尚未檢查以及沒有檢查的任何查詢。模擬連線只返回你所需的資料。

不建立整合測試意味著你實際上沒有測試你的應用程式。

不測試資料庫Schema(模式/架構)

我所觀察過的大多數團隊擁有某種形式的整合測試。通常進行快樂路徑測試:有某個ORM工具,我們持久化物件,ORM工具會完成那些工作,真是太酷了,我無須費心。

我從未見過一支對資料庫schema(模式/架構)進行測試的團隊。想象一下,由於某些針對產品的查詢很慢,因此你必須在該資料庫中建立某個索引。當下次在新的客戶環境中部署此應用程式時,你希望擁有該索引並確認該索引真的就在那裡。為什麼不編寫一個簡單的測試來檢查該索引的存在呢?

除了索引,還有許多要測試的內容:

  • 主鍵(primary keys)
  • 外來鍵(foreign keys)
  • 一些檢查——以確保“price”(價格)列不會有負值
  • 某些列的唯一性(uniqueness)——你實際上不想擁有兩個具有相同登入名的使用者。

不在生產環境下測試

當你開發某個應用程式時,你可以從種類繁多的資料庫中進行選擇。通常你會從中選擇那個最好的、那個被團隊所熟知的、或者是由管理層所選定的(有時使用一些奇怪的理由)。有時同一應用程式的多個部署會在同一時間使用不同的資料庫引擎。有時應用程式會為了能使用不同的資料庫引擎進行準備,因此購買此應用程式的客戶就可以選擇他想要的資料庫。

資料庫引擎的選擇真的與進行產品測試無關。

由於程式設計師的懶惰,因此他們希望他們的測試可以執行得飛快。他們不想為測試結果等太久。這也就是為什麼許多團隊使用某些更快的記憶體資料庫(例如HSQLDB)的原因。由於那些記憶體資料庫僅儲存在RAM(Random Access Memory,隨機存取儲存器)記憶體中,而且在操作時不接觸任何硬碟驅動器,因此它們的執行速度要快得多。

還有一條常常被人遺忘的規則:

測試應該使用與生產環境相同的資料庫引擎。

許多程式設計師會使用某個其他引擎。常見的解釋很簡單:“我們的資料庫太慢了,我們應該使用某個記憶體資料庫引擎。”。這並不是個好主意。這樣你測試的是該其他引擎,而非你的生產環境中的那個,所以實際上你並沒對你的應用程式進行測試。

我曾經遇到過這個問題。我們必須在成功連線資料庫後通過設定session變數來優化查詢。那個應用程式在生產環境中只使用Oracle資料庫。當設定此變數以後,測試失敗了。而且是所有的測試都失敗了。原來是我不能在HSQLDB記憶體資料庫中設定此變數,因為它根本不存在。因此,我必須編寫一段糟糕的程式碼:在連線後,檢查資料庫引擎,並由此決定是否設定此變數。

即使你沒有任何與混合引擎有關的問題,請記住,當你測試其他非生產環境的資料庫引擎時,你恰恰根本沒對你的應用程式進行測試。

不準備好資料庫就進行測試

測試有一個通用的、明確定義的流程。 它非常簡單:

  1. 準備環境
  2. 執行一個測試
  3. 檢查測試結果
  4. 返回至第一點

若嘗試背道而馳,則你會被它所傷。

你察覺在測試之後是沒有整理(tidying)的麼?

這點非常重要:必須在測試前準備環境,而非測試之後。你無法確保測試將能夠清理一切。應用程式可能因為某個錯誤、網路連線失敗而退出,或者應用程式可能崩潰(例如,由於記憶體不足異常)。該測試如何終止並不重要,真正重要的是該測試執行在為每個測試所準備的相同環境中。

我曾犯過這個錯誤:有大量的整合測試,它們會在每次測試後清理所有更改。許多程式設計師正使用偵錯程式來執行這些測試,並且當發現bug後會在中間停止測試。任何在該測試之後執行的測試會得到不可預知的且隨機的結果。因為它正執行在已被前一測試所改變的環境中,而且沒有為其清理整個環境。

準備了資料庫卻不對其檢查就進行測試

在前面的部分中我提到許多有關準備資料庫的內容。我還想補充一點。準備資料庫是不夠的。當你通過清理某些表、載入配件等等準備好資料庫時……還剩下一件事情要做。

在準備完畢後,要檢查資料庫狀態。

你真正需要確保的是你已將一切準備妥當。當出現由於bug所導致的某些資料留下來且未能清理時,這些工作就可以節省你的時間。

這應該是測試前資料庫準備的一部分。

不測試建立指令碼

每個應用程式都需要某種形式的安裝過程。而對於你部署資料庫而言,永遠是第一次。

程式設計師往往會通過手工執行某些臨時的資料定義語言(DDL,data definition language)查詢來改變資料庫。他們稍後並沒有把那些語句寫下來,或是忘記了所做的改動。他們沒有更新安裝指令碼。大多數團隊不使用有版本控制的指令碼(例如,Ruby on Rails中的Migrations、或者是Java世界中的Liquibase)。

對測試而言最好的方式是,在執行測試套件前重新建立資料庫。你不必在每個測試開始前都那麼做。只在執行所有測試前執行一次就行了。

是的,寧可事先謹慎有餘,不要事後追悔莫及。

不測試外來鍵

外來鍵是提供資料庫一致性的基本途徑之一。在良好的關聯式資料庫schema中,你應該擁有各種鍵。如果你沒有的話,那麼這可能表明你有一個真正的大問題。然而,這取決於資料模型,但是通常缺乏外來鍵是種非常糟糕設計的味道。

測試外來鍵很簡單。只需在事先沒有在引用表中新增適當的行的情況下,為某個表新增一些行。你應該得到一個錯誤。然後,你應該從引用表中刪除行,你可能得到錯誤,或沒有錯誤(這取決於該鍵的定義)。無論如何,你都應該檢查一下預期行為。

不測試預設值

在良好的資料庫設計中,你應該定義一些合理的預設值。通常這些預設值可能是空(null)。即便這些空也應該進行測試。你不能假設,只有你的應用程式將改變此資料庫中的資料。\ 兩個問題:

  • 如果某人想建立一個快速修復並使用臨時SQL查詢更新某些資料,該怎麼辦?
  • 如果有一天某人啟動另一應用程式,它可以改變這些資料,而且新的應用程式將不使用你的ORM對映或你的DAO(資料訪問物件)類,又該怎麼辦?

如果你擁有愚蠢的預設值、或是錯誤的預設值,那麼你可能會破會資料,而且那可能比一個簡單的應用程式bug更糟糕。

不測試約束

在資料庫中還有更多約束,不僅只有主鍵和外來鍵約束。你可能擁有一些唯一的(unique)或不為空的列。你可能約束某列只有很少的值集。你可能想確保價格永遠不會低於0。

良好的資料庫schema應擁有許多約束。你也應該測試它們。如果你希望你的價格列只能擁有正值,當你嘗試向其中插入-1美元時會發生些什麼?為什麼不測試一下呢?

你不能假設只有你的經過良好測試的應用程式將使用那些資料,而且這些檢查是為你防禦這些bug的最後一道防線。為什麼不測試它是否正常工作?

多個測試可以更改同一資料庫

通常,資料庫測試會更改資料庫。你可能同時執行多個測試,但是你必須確保那些測試彼此之間沒有影響。你必須確保,如果某個測試將一些內容寫入資料庫,而另一測試將不會讀到。

通常,很容易搞得一塌糊塗,因此我小小的忠告是:避免在同一時間執行多個測試。這也意味著,你不應該在多臺機器執行相同的測試套件。

當你有許多想執行測試的開發者時,他們每個人應該擁有可用於編寫測試的單獨的資料庫。如果你擁有某種形式的只讀資料庫,沒關係,多臺機器可以在同一時間使用這個資料庫。但是如果你允許所有程式設計師使用同一資料庫進行測試的情形出現,那麼你可能真的會得到不可預知的測試結果。

當程式設計師在某個測試中發現一個錯誤時會怎麼做?那麼,優秀的程式設計師會盡量修正錯誤。如果該測試失敗僅僅是因為另一程式設計師在同一個資料庫上執行他的測試所導致的話,那麼修正此類錯誤只是在浪費程式設計師的時間。

沒有大紅按鈕

優秀的程式設計師是懶惰的。如果你命令優秀的程式設計師每次都重複同樣的任務,他們會越來越沮喪。優秀的程式設計師會自動化可重複的事情。

在每個專案中,你必須在測試環境中部署某些東西。做這些會花去多少時間?你真的想為了重新部署應用程式和載入資料庫一直浪費你的程式設計師時間麼?

這就是為什麼每個專案都應該有個大紅按鈕的原因。某位程式設計師可以按下此按鈕,然後衝杯咖啡,回去工作,並且幾分鐘後得知的大紅色按鈕完成的工作,一切準備就緒。

大紅按鈕真的會為你節省很多時間。你會說自動化所有工作實在太緩慢。然而,事實並非如此。恰恰相反,就像說測試驅動開發(TDD,Test-Driven Development)很慢一樣。在最初的時候比較慢,但隨著專案變得更加複雜,由於存在測試或按鈕,會為你節省更多的時間。各種各樣的大紅按鈕——你可以用它們部署應用程式、執行測試,以及類似的後方支援。

有時會使用Jenkins(又名Hudson)來做這些。是的,對於此類大紅按鈕而言這是一款偉大的軟體。唯一的事情是,每位程式設計師應該有其自己的一組工作以便在其自己的環境中部署所有的內容,在其自己的環境中他(或她,當然)可以自由發揮,而不會影響他人,同樣也不會受到他人的影響。

工具

有許多資料庫測試工具。為了測試整個schema,你可以編寫簡單的整合測試。對於PostgreSQL有pgTAP,使用TAP外掛它可以與Jenkins整合到一起,因此Jenkins可以擁有一項用於測試資料庫(包括生產資料庫)是否正常的工作。

相關文章