Chapter 4: What Makes Great Tests

當 Rails 成為熱門的開發網頁框架之一,許多的 Rails 開發者也開始願意使用 codebases(代碼庫)以及 test suites 來工作。 相對之下,卻沒有足夠的討論在延續未來開發軟體的議題上,來探討如何讓測試能夠變得更好。

此章節就是要討論以許多不同的方法來評估測試的價值及成本。

The Big One

有關於自動化測試的風格及結構,作者認為最好的建議是: 你撰寫的程式碼是被測試來驗證的,但是你撰寫的測試卻沒被任何東西驗證。因此最好讓你的測試能夠ㄧ目瞭然,及方便管理。

實際上,這代表你最好遠離複雜的 metaprogramming 或是使用迴圈在撰寫測試程式碼時,因為這除了會增加除錯的難度,也比較不容易理解。

“Your tests are also code. Specifically, your tests are code that does not have tests.” ― Rails 5 Test Prescriptions ―

如果你撰寫的編碼或是工具是成功的,使用它會使得以下項目更輕鬆:

  1. 能在短時間內,輕鬆地加入你需要的程式碼
  2. 隨著時間的增長,能不斷地加入程式碼在專案中

” Testing is normally thought of as working toward the second goal. That’s true, but often people assume the only contribution testing makes toward long-term application health is verification of application logic and prevention of regressions. In fact, over the long term, test-driven develop- ment tends to pay off as good tests lead toward modular designs. “― Rails 5 Test Prescriptions ―

Cost and Value

測試都會有它的成本和可以獲得的價值。 測試的目的就是想辦法最小化成本, 以及最大化可帶來的價值。

測試成本是顯而易見的,以下幾點都是測試的成本:

  1. 撰寫測試所需要的時間

  2. 跑測試所需要的時間

  3. 了解測試所需要的時間

  4. 修正測試及恢復系統程式碼所需要的時間

  5. 優化程式碼讓測試可以跑起來所需要的時間


當然除了以上列出的成本外,撰寫測試也有許多好處的,以下是書中的幾個例子:

  1. 撰寫測試可以讓定義程式碼的結構更為簡單

  2. 跑測試通常是自動的會比手動更快

  3. 測試是更有效率地證明程式碼是正常運行

  4. 測試能夠警告程式碼的改變會導致在系統上行為的變化。

  5. 測試能更簡易及快速地找到錯誤並且修正它

Example of low cost & low value

當使用 Shoulda gem所提供的以下 matcher 來描述物件之間的關聯時,可以看出這是很簡單的ㄧ句話就可以完成的測試,並不需要花費太多時間來編碼。換句話說,這是以低成本完成的測試。然而,此測試卻也是只能提供少許價值而已。

  describe 'associations' do
    it { should belong_to(:category).class_name('MenuCategory') }
  end

原因是這個測試是立基於資料庫,而不是 code logic,所以並沒有說明太多有關於程式碼的設計。 這個測試獨立出來驗證是很難有什麼問題出現。 假設 ‘MenuCategory’ class 與 category 的關聯消失了,許多牽扯到該 association 的測試也會跟著出問題,但測試失敗的訊息也只說明了我們已經都知道有關於程式碼狀態。

Example of high cost & high value

從另一方面來說,想像ㄧ個用 Capybara 來建置的 end-to-end 系統,類似在上一章節中所撰寫的測試。這樣測試的成本是很高的,因為它需要大量的資料、許多測試步驟、跟複雜的 output matching。但同時,如此的測試是含有極高的價值,因為它也許是唯一的測試能夠連接串通不同的小細節,並且告訴你哪個環節出了什麼問題。 除此之外,通常讓系統自己跑測試會比你實際上手動操作測試來得更有效率、快速許多。

以下指導準則可以幫助我們減少測試成本跟最大化帶來的價值:
  1. 不要只想著如何讓測試通過,應該去想什麼情況會促使測試失敗? 如果沒有辦法使測試失敗,也不會導致現有測試失敗,那麼也許你不需要這個測試。

  2. 思考如何建立整合性測試(Integration tests):這樣的測試能藉由自動化一系列行為幫助你節省時間,同時在開發軟體上也很實用。

  3. 以單元測試(Unit tests)來說,撰寫測試時應該思考如何能用最少量的程式碼,就足以讓測試失敗為考量。之後Chapter 7 將會解說如何使用 “Test Doubles as Mocks and Stubs” (page 129),失敗的案例通常可以被衍生成為單元測試,而不是建立成為更慢、更複雜的整合性測試。

  4. 試著遠離高成本的活動,例如:呼叫外部的函示庫、或是儲存一堆物件在資料庫中。 在測試裡,可以用 test doubles 避免單元測試必須要使用 real dependencies。

  5. 如果你有找到單ㄧ個 bug 使得許多的測試失敗,要思考是否那些失敗的測試有存在的必要。 如果是在 setup 造成的,就應該考慮是否那些失敗的測試有需要 setup 的需要。

  6. 有時候測試會只有在開發階段很有用,但之後會被其他的測試取代。 這樣的測試就可以被刪除。

  7. 如果你發現某個單元測試需要花很多的 setup 來完成, 這代表該測試是嘗試跟你說『程式碼的設計需要改善來處理 dependencies 問題』。