單元測試分享(一) --- Why ? How ? What ?

前情提要

目前參加的讀書會有點進入了倦怠期,
參加的人數出席不穩定,會議過程有點感覺像是在照本宣科。
所以我們安排每隔幾周,由不同分享一下個人工作上不同的經驗。
讓負責分享章節的人可以有更多時間準備,
同時也可以讓聽者換換口味同時喘口氣。

對於我來說,可以重新整理一下過往的經驗。
試著能不能更有組織的分享單元測試這個工具與知識。
同時,因為是線上分享會的形式,當我無法觀察觀眾的表情時,
遠端會議應該怎麼進行才能更有效呢?

遠端協作的 Conversation Cost 仍然相當高

進行方式

  • 先問為什麼?
  • 輪流發表一下想法(TimeBox:60s)
  • Live Coding (如果是現場分享,我會希望多一點實作)

思考問題

  1. (單元)測試是什麼呢 ?
  2. 為什麼要寫(單元)測試 ?
  3. 什麼樣的情境需要(單元)測試?

這裡的設計是 What、Why、When,我覺得改成 What、When→How、Why 會更好。
主要的目的是引導聽眾思考 Why,這裡我的受眾應該為開發人員,
實務上還有可能會是 QA(測試人員)、產品經理(PM/PO*)、維運人員、
營運部門甚至是老闆或客戶。

舉例情境如下:

問:單元測試或是測試是什麼呢?
答:是一種自動化的對程式的方法進行驗証 blab blab ….

捕捉到關鍵字 自動化 方法…可以延伸提問

  • 測試不一定要自動化,但是單元測試建議要作到一鍵測試(半自動)
  • 單元不一定是方法,但我們可以以方法當作最小粒度來討論,
    後續的分享會對粒度再作討論,但重要的是團隊的認知要一致

進一步提問**什麼樣的情境需要(單元)測試?**,
如果現場乾掉了,可以改成以下的提問:

  • 上 Prod 前要不要測試?
    • 那上 QA/Stage 要不要測試?
  • 開發新功能要不要測試 ?
    • 那 HotFix 要不要測試 ?
  • 拿到一份全新沒看過的 Source Code 要不要測試 ?

你會發現測試無所不在,那我們為什麼(Why)要測試啊?
我的回答,(單元)測試是品質保証的一種手段,

如果上線前,(單元)測試全過,
我就對被測試保護的方法(情境)有信心不會壞。
同樣的,在部署到其它環境時測試通過,我對品質信也就會跟著提昇。
換句話說,測試是品質的可量化指標。

可能的問題:測試全過,上線還是有可能會壞掉啊
回答: 壞掉是什麼樣的情境 ? 是不是一種沒有被測試保護到的情境 ?
是的話只要加上情境即可,一般來說我們應該可以作到 96 % 以上的常態情境
剩下考慮發生機率與重要性,應該加上測試就加測試保護,
這裡不限定單元測試也包含整合/端到端/手動等其它測試。

另一個情境是開發新功能,
任何功能都是方法與流程的組合
當你在開發新方法或新功能時,如果能同時寫好測試*,
那你就會有一份具備可量化品質的代碼
同時還可以帶來的額外好處是,當有一個新人拿到你的代碼時,
他會有一份測試可以當作規格書來閱讀。

注意:這裡可以代入很多其它相關的概念
比如說:TDD、Code Review、可讀性、Pair Programming

理解方法與流程是最耗時的一件事,
如果有測試可以節省相當多的時間,
但要注意測試也是人寫的,如果為了寫而寫,很容易寫出垃圾測試,
試著在開發流程上引入 Pair Programming、Code Review、Pull Request 等…機制
另外當代碼成長到一定程度時,
如果需要重構,測試將提供一定的保護網(看覆蓋率多少)。
但要小心實務上不應該過度追求覆蓋率。

這裡提供一個觀念,不要為了 Design Pattern 而 Design Pattern
但是即有的代碼需要重構之時,Design Pattern 可以提供像是燈塔般的指引作用
只要巧妙的設計測試案例,走向目標。

此外,當代碼有了單元測試的保護,
在開發日常的除錯作業將會有很大的幫助,
測試可以幫你快速的定位錯誤。
即使測試並未攔截到錯誤的話,那也表示你發現了一個前所未有的情境,
而只要加上這個測試情境,再修改代碼,未來這個情境將不會再有錯誤。

經典書籍對單元測試的定義

1
2
3
4
5
6
7
一個單元測試是一段自動化的程式碼,這段程式會呼叫被測試的工作單元,
之後對這個單元的單一最終結果的某些假設或期望進行驗証。
單元測試幾乎都是可以使用單元測試框架進行撰寫的。
撰寫單元測試很容易,執行起來快速。單元測試可靠,易讀,並且很容易維護。
只要要產品不發生改化,單元測試執行結果是穩定一致的。

--- <<單元測試的藝術2nd>>

F.I.R.S.T Principles

1
2
3
4
5
6
7
- Fast : 快速→不夠快就不會被頻繁執行
- Independent :獨立→互相依賴的測試,會讓除錯變得困難
- Repeatable : 可重複→在任何環境下執行都有相同的結果(EX:時間/網路)
- Self-Validating : 自我驗証→測試是否通過,不需額外的判斷與操作
- Timely : 即時→產品代碼前不久先寫測試

---<<Clean Code>>

小結

上面提到很多工程面的實踐,
也不僅限於單元測試,更多的是測試與品質的關係。
一再強調的觀念是:

測試是品質的可量化指標。
測試是品質的可量化指標。
測試是品質的可量化指標。

而回到工程面上來說,作為第一線的品質把關,
開發人員本來就應該對自已的代碼提供單元測試,
而 TDD、Test As Document、Refactoring、Design Pattern 等…是環環相扣的工程實踐,
Pair Programming、Code Review、Pull Request 等…是工程流程的實踐
單元測試正是那個將各種實踐結合在一起的一種工具。

Unit Test is basic tool

在心法上,要將單元測試視作工具,而非聖盃,
TDD 或是覆蓋率也是同樣的道理,御物而不御於物。

1
2
可用的軟體 重於 詳盡的文件
<<敏捷軟體開發宣言>>

引用一下敏捷宣言:
可用的軟體 重於 100%覆蓋率的代碼
可用的軟體 重於 測試全過的代碼
右側項目雖然有其價值,但我們更重視左側項目。

而這裡可用的最低標準,我認為是符合品質期待

First Test

第一個單元測試,加法計算器

Case:Add(+) : 1 + 1 = 2

1
2
3
4
5
6
[Fact]
public void Add_1_1_is_2()
{
var target = new Calculator();
Assert.Equal(2, target.Add(1, 1));
}

Production 代碼

1
2
3
4
5
6
7
public class Calculator
{
public int Add(int first, int second)
{
return 2;
}
}

Next Case

用 Test Case 逼出邏輯,用最簡單的方法實踐

1
2
3
4
5
6
[Fact]
public void Add_2_1_is_3()
{
var target = new Calculator();
Assert.Equal(3, target.Add(2, 1));
}
1
2
3
4
5
6
7
public class Calculator
{
public int Add(int first, int second)
{
return first + second;
}
}

3A 原則

  • Arrange(準備、初始化)
    如果 Arrange 過長會是一個壞味道,
    表示這方法相依太多參數、服務或模組
    範例:

    • target = new Calculator();
    • first number is 2
    • second number is 1
  • Act(執行/呼叫受測行為)

    • target.Add(2, 1)
  • Assert(驗証)

    • `Assert.Equal(3, acted result);

不要過度追求可讀性,而將測試程式變得難以理解,
可以使用測試驗証框架(ex:Fluent Assertions),
或是抽出方法來增加可讀性,但比起可讀,更重要是可理解。

彩蛋

後續的 Live Coding 會讓參加者完成後續的方法測試,
加法、減法、乘法、除法…
特別計算到除法的除零邏輯時,應該拋出錯誤。
趁這個機會可以介紹如何驗証 Exception。

進一步可以介紹如何使用 fluentassertions

1
2
3
4
5
6
[Fact]
public void Divide_7_0_is_Exception()
{
Func<int> act = () => _target.Divide(7, 0);
act.Should().Throw<DivideByZeroException>();
}

(fin)

Please enable JavaScript to view the Gitalk. :D
Please enable JavaScript to view the LikeCoin. :P
Please enable JavaScript to view the LikeCoin. :P