筆記:重構 — Chapter 4:打造測試

YH Yu
後端新手村
Published in
6 min readAug 10, 2019

系列文章:重構 — 改善既有的程式設計

這個章節作者拿一段程式碼作為範例,在逐步添加測試的過程中,帶出測試的相關原則和重要性。

本文會用以下順序,重新整理這章的重點:

  1. 測試的價值
  2. 測試的觀念
  3. 撰寫測試的注意事項

測試的價值

還記得先前在重構原則中提到,花時間重構的最終目的,其實是為了節省時間。為程式碼撰寫測試也是同樣的道理,可以說“花時間”寫測試的最終目的也是為了“節省時間”,為什麼呢?

回想一下工程師的日常,花費在除錯上的時間,和實作功能比起來是不是相對長上許多,過程也更加乏味?而進行除錯時,大多時間也是花在定位錯誤而非修改程式碼本身。

A suite of tests is a powerful bug detector that decapitates the time it takes to find bugs.

測試是為了確保程式碼的穩定性,並希望有錯誤時能快速定位到問題的程式碼,提升我們開發的效率。

以重構的角度來說,由於會頻繁地更改既有的程式碼,因此加上測試來保護是不可或缺的。缺乏測試會讓我們不敢重構、不想更動架構,或是習慣性地用權宜的方式解決問題(害怕破壞程式碼)。持續堆疊這些品質不佳的程式碼又會讓添加測試的難度提高,形成一個惡性循環!

測試的觀念

其實重構所需要的測試,和一般實作功能時並沒有什麼不同。這本書主要還是著重在重構上,測試的部分則以介紹一些重要的觀念為主。

我先前有寫過一篇閱讀心得—軟體測試的反面模式。這個章節中提到的大多觀念其實都有涵蓋在裡面了,重複的部分會直接引用不再贅述。

以下是這個章節所帶到的測試觀念:

盡可能頻繁地執行測試

正在修改的程式碼,最好每完成一小部分,或是隔幾分鐘就跑一次相關的測試。而完整(所有)的測試每天至少要跑一次。

不要忽略失敗的測試

若當前的程式碼無法通過測試,在修好它之前我們不應該進行任何動作(e.g. 重構、實作功能)。否則測試的結果會變得更加發散,難以判斷後續修改的部分,是否破壞了原有的程式碼。

優先測試重要的程式碼

我們常會”逐一”地為專案添加測試而沒有先仔細思考,不同職責的程式碼,出錯時的嚴重性(severity)自然也不盡相同。

資源有限的情況下關鍵的程式碼要優先測試,不應該花太多時間在測試相對不重要的功能,甚至是幾乎沒有出錯可能的程式碼(e.g. getter/setter)。

將回報過的錯誤加入測試

除了我們根據業務邏輯判斷出的關鍵程式碼外,還有一種情況也需要優先納入測試,那就是由使用者回報的錯誤!

它代表我們目前的測試沒有涵蓋到,卻真的會對使用者會造成影響的錯誤程式碼。將它加入測試,保證未來不會在同一個地方再次出錯。

不要過度執著於測試覆蓋率

為了追求測試覆蓋率,有些測試可能需要付出龐大的成本,卻沒有太大的效益。以測試的 80/20 法則來看,覆蓋率高到一定程度後,就很難再添加真正有用的測試。

況且就算是 100% 的測試覆蓋率,也不代表程式碼沒有任何的錯誤。測試覆蓋率(test coverage)比較適合作為參考,看看專案中有多少比例的程式碼沒有測試。

測試邊界條件

Think of the boundary conditions under which things might go wrong and concentrate your tests there.

不要只測試happy path,也就是一切順利,沒有任何錯誤發生的情況。就算機率再小,壞事總有可能發生!一旦錯誤發生時,若程式碼無法正確地處理,就可能導致嚴重的後果。

在撰寫測試的時候,盡力扮演自己程式碼的敵人(playing the part of an enemy to code),確保程式碼在不同條件下都能穩定運作。

不完整的測試也勝過沒有測試

綜合以上所提,即使我們的測試沒有涵蓋到所有的程式碼,或是沒能抓到所有的錯誤,也不需要就此完全捨棄測試。

只要把握上述原則,即使一開始只有少量的關鍵測試,就已經足以大幅提升專案的開發效率和穩定度。

撰寫測試的注意事項

確保測試有正確運作

Always make sure a test will fail when it should.

常常發生在撰寫時,測試本身就沒有正確地被實作。像是其實根本沒有執行到想要測試的程式碼,導致即使程式碼有誤,還是能夠順利通過測試。

因此在寫完測試後,首先要做的並不是讓程式碼通過測試,而是故意在程式碼中加入錯誤,看看測試是否會如預期的失敗。是的話再把這個錯誤拿掉,然後執行正式的測試。

小心處理共用的Fixture

所謂的 fixture,指的是為了測試而設置的資料和物件。例如本章範例:

這邊我使用的測試框架是 Jest 而非書中的 Mocha

其中常數noProducers就是 fixture,但這邊的用法是一個不好的示範!

多個測試共用同一個 fixture,有可能造成測試間互相影響,導致未預期的結果。較好的做法是為每一個測試,個別建立全新的 fixture,例如:

beforeEach是每個測試在建置階段(setup,測試開始前)會執行的函式,常見的測試框架(e.g. JestMocha)都有支援類似的用法。

大多數情況我們都應該避免在測試間共用 fixture,除非:

  1. 確定 fixture 不會在測試過程中被改變
  2. Fixture 初始化的成本很高不得不共用。這種情況要注意在拆除階段(teardown,測試完成後),將被改動到的 fixture 復原。

如何處理Assert階段外的錯誤

若錯誤發生在檢驗測試結果的階段(e.g. expectassert)之外,通常表示這個錯誤不在預計的測試範圍內。例如傳入了不支援的格式、錯誤的呼叫方式等等。

可以分為兩種方式應對:

  1. 呼叫端來自外部,例如透過 API 呼叫,那麼需要修改程式碼做適當處理。
  2. 呼叫端是可信任的(保證會以正確的方式呼叫),或是我們正在進行重構(不改變程式碼的現有行為),那麼可以考慮不處理。

不要在一個測項同時檢驗多個項目

每個測試都應該要有一個非常明確的目的,檢驗的目標必須非常明確。當測試失敗時,我們就可以快速定位到相關的程式碼。

除非測試項目彼此的性質非常接近,否則將多個測試項目放到同一個測試中的話,當第一個檢驗失敗時整個測試就會中止。這可能會導致我們無法明顯分辨出程式碼總共有哪些部分被破壞,提高除錯的難度。

總結

如何衡量測試的品質?一種直接的方式是問自己『若有程式碼被破壞了,我們有多大的信心認為測試真的會失敗?』或者反過來說,當所有的測試都通過時,我們是不是有把握說這就是一個可靠的版本?

撰寫測試是一個迭代的過程,就算是老手再加點幸運,也很難一次到位。對待測試要像對待一般的程式碼一樣,為了維持良好的測試架構,需要時常去檢視、重構,確保它們真的能持續幫助我們提升開發效率!

如果這篇文章對你有所幫助的話,歡迎拍手讓我知道,最多可以拍50下喔👏👏

--

--

YH Yu
後端新手村

Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better.