淺談單元測試 - 管理篇

上次簡單介紹單元測試帶來的好處,今天要換從另一個角度思考--如何讓這個好處帶入自己的團隊裡。

若團隊本身已經習慣在開發流程中做單元測試的,那這篇文章可能就幫不上什麼忙,由成員來說明開發流程會比較直接;若團隊還沒有養成這個習慣的話,希望這篇文章能激發一些想法,同時幫上一點忙。

以下方法是我個人有用過,但不一定符合所有情境,可以當作經驗分享討論。

少說少錯,不說不錯

人是習慣性的動物,改變習慣對大部分的人來說是痛苦的。當提到要寫單元測試時,不同的角色會有不同的聲音。以下是我曾經聽過的,如有雷同純屬巧合:

  1. 工程師:「我不會寫,而且這會增加我的工作量。」
  2. 主管:「做這個要多久?」
  3. 產品擁有者:「那上次講的 XX 功能什麼時候才能上線?」
  4. 老闆或股東:「做這個會賺多少錢?」

會有這些狀況,是因為一開始沒有養成這個習慣,因此當提出要寫單元測試時,就會被認為是流程或方法上要求有所改變,那自然比較容易抗拒。

先提一個很基本的概念:工程師寫完程式後,要確認寫出來的程式是正確的才能提交;若不驗證就提交,那就會是 bug 產生器,相信大部分的人都能認同。至於要怎麼驗證,比方說把程式放到主機上直接做驗收測試,是一種方法;而單元測試也是一種驗證方法。因此,如果打算開始寫單元測試的話,其實沒有必要特別提出--本來自我驗證就是必要的工作了不是嗎?

評估寫程式的時間是可以包含單元測試的,會需要額外評估的,應該是學習或練習單元測試的時間,但這也可以包含在寫程式裡。如此一來,工程師的工作,就只有寫程式這件事而已。不要提到關鍵字「單元測試」,對老闆或產品擁有者而言,或許就比較容易買單,因為沒有流程上的改變,只有時程上的調整。

當然,時間拉長也有可能不被接受,這時可以考慮分解任務後再針對某幾個簡單的任務做單元測試,如:

  1. 簡單的身分驗證模組加單元測試從 4hr => 6hr
  2. 困難的權限管理 8hr 則不變

這樣整體時間會從 12hr 變成 14hr,這樣相較就會平緩可接受一點。建議選簡單的原因是,開發時間、程式複雜度、與寫單元測試的難度成正比,因此開發時間小的任務,通常比較容易寫單元測試。另外也要設定停損點,若真的無法完成,要即時改用原本的作法驗證程式。

微小改變,巨大效益

工程師是實作的角色,因為沒寫過單元測試,需要花時間學習、預估時間困難、甚至有年資的工程師還會有「我寫的程式怎麼會有 bug?」的想法。若身為主管,有權限要求成員達成主管所制定的目標時,可以參考下面的建議;若不是主管,可以跟主管討論並以身作則,持續執行下面這些建議,也是有機會改變同事。

整理測試案例

在不寫單元測試的時候,是如何驗證程式的正確性?相信每個開發者都有自己一套測試的方法,但建議要求開發者把測試案例用一行文字寫出來。比方說,登入行為至少會有兩種測試案例,如登入成功與登入失敗,這時就可以把這樣的文字轉成單元測試的測試案例(以 PHPUnit 為例):

/**
* @test
* @testdox 當輸入正確的帳號密碼時,會回傳登入成功
*/
public function shouldReturnTrueWhenInputCorrectUsernameAndPassword()
{
$this->markTestIncomplete();
}

/**
* @test
* @testdox 當輸入錯誤的帳號密碼時,會回傳登入失敗
*/
public function shouldReturnFalseWhenInputIncorrectUsernameOrPassword()
{
$this->markTestIncomplete();
}

PHPUnit 的測試有個參數是 --testdox,它會把上面測試案例註解寫的中文印在畫面:

Login (Tests\Feature\Login)
∅ 當輸入正確的帳號密碼時,會回傳登入成功
∅ 當輸入錯誤的帳號密碼時,會回傳登入失敗

上面的 符號是單元測試未完成的意思。接著就可以開始寫程式了,完成程式後要手動測試或寫單元測試都可以,而單元測試就會把上面未完成的符號消掉,如:

Login (Tests\Feature\Login)
✔ 當輸入正確的帳號密碼時,會回傳登入成功
∅ 當輸入錯誤的帳號密碼時,會回傳登入失敗

當然開發會有時程的考量,也許只寫一個測試就提交也有可能。而這樣做有幾個好處:

  1. 開發前可以透過測試案例了解需求,先不管這是 TDD、BDD 還是 SBE,引用 Dijkstra 說的:「用證明的方式來確定程式的正確性」(參考 Teddy 關於BDD/TDD的三大誤解的誤解二段落),上面的方法正是在實踐這句話。
  2. 其他開發者在執行單元測試的時候,會知道哪些測試沒有自動化測試,而需要手動測試。
  3. 在 code review 的時候能幫得上忙--reviewer 能從測試去了解程式的行為,進而達到快速了解開發者設計的樣貌。

先求有,再求好

以功能測試角度來說,單元測試是符合八二法則邊際報酬遞減法則的。

在有時間成本的考量前提下,找出重要的業務邏輯或是高風險程式優先處理,將會非常有效益。又因為邊際報酬遞減法則,只要開始有寫一點,帶來的效益就會非常大。因為每個提交的程式都需要重新檢視,因此最後只要簡單要求成員做一件事就好:

測試不用多,有,且重要就好!

就我的經驗而言,假設一個提交的程式需要寫 10 個單元測試案例(test case)才算完整測試,只要找出重要的 1 ~ 2 個測試案例並寫成單元測試,就能幫上非常大的忙了。最難的只有專案的第一個測試,後面通常複製貼上改一下就能跑,簡單很多。

時常執行,保持新鮮

程式寫了,要有人去執行才能產生價值,單元測試也是程式,把它加入持續整合的流程裡,才能產生最大的價值。

我曾經有過的經驗是,寫了單元測試但沒有持續整合,因此曾經發生過程式被改壞但全然不知的狀況。因為單元測試只有我會執行,其他人不會,因此無法即時反饋單元測試錯誤需要修改的問題。

因此,寫單元測試只是開始,安排執行的時機點也是非常重要的,就以持續整合的角度來說,普遍的狀況會是每次提交(commit)程式都要執行一次單元測試,而在請求合併(pull request / merge request)時再做完整的測試,當然這就會視團隊狀況與產品性質來決定。

小結

雖然上面講了這麼多,事實上單元測試要從無到有,還是要由專業的教練來協助團隊導入單元測試才是最快捷的方法,像是 91 開的針對遺留代碼加入單元測試的藝術,正是針對從無到有所做的教學。雖然我沒有上過,但與 91 互動的感覺非常棒,而且認識很多社群朋友都說好,真心值得推此課程。

花錢買時間是一種方法,若不想花錢的話,原則上就只能慢慢去改變公司開發的習慣,包括眾多開發者、管理者、產品擁有者、甚至是老闆的思考習慣,有朝一日一定能能成功的。


感謝 @tedmax100@yaochangyu@jame2408 等好友提供許多寶貴的經驗參考。