Unit Test

  1. 1. 軟體開發故事03 - 單元測試 Unit Test 與 測試趨動開發 Test Driven Development (TDD)
    1. 1.1. Ready, Go!!
      1. 1.1.1. 寫測試
      2. 1.1.2. 跑測試亮紅燈
      3. 1.1.3. 改code
      4. 1.1.4. 跑一下測試,綠燈
      5. 1.1.5. 重構,跑一下測試,綠燈
    2. 1.2. 反思一下我們在上面的所有動作
    3. 1.3. 想一下,不寫測試不行嗎?
      1. 1.3.1. 先寫代碼,再寫測試不行嗎?
    4. 1.4. 再次強調,在很多情況之下,我們是很相信工程師的
    5. 1.5. 工程師有沒有可能出錯? 那怕是千萬之一的機會,產品如果有問題,公司、團隊、個人能不能承擔犯錯之後的結果?
    6. 1.6. 工程師能不能信用擔保,或者用性命擔保程式沒有什麼?
    7. 1.7. 寫了測試就不會出錯了嗎?
    8. 1.8. 應不應該追求所謂的100%的測試覆蓋率(100 Test Coverage)?

軟體開發故事03 - 單元測試 Unit Test 與 測試趨動開發 Test Driven Development (TDD)

前情提要:工程師1有可能寫出一個功能,裡面的程式能通過測試用例,QA也沒測到,但是上線實測有可能會壞掉的嗎?

相信你應該有答案,那麼,為什麼要寫單元測試?

因為公司要求,因為要被code review,因為要導CI、CD,因為別人bla bla bla…

自己的想法呢?程式都沒時間寫了,不寫測試我可以多寫幾千行程式

大多時候工程師是一個不太好被別人說服的角色,那麼,我們來模擬一下工程師1怎麼使用SPE來同時寫出單元測試和實體程式

事前先準備好

  1. 單元測試所需要的環境
  2. 實際程式的環境

Ready, Go!!

第一步,看一下我們的user story,先把測試給寫出來

1
2
3
假設(Given),存在有一個可用的帳號與密碼
當(When),使用者開啟APP,輸入帳號 aki ,密碼 qaz,點選「登入」
然後(Then),要看到首頁的內容。(success)

test case 如下:

1
2
3
4
5
6
//============== Test ==================
func testLoginSuccess() {

XCTAssert(login(acc: "aki", pass: "qaz"))

}

如上,我們寫出了第一個Test Case,此時試著跑一下Test,會得到 Test Failed,因為login函數還沒有實做。此時我們把Failed的狀態簡稱「紅燈」。

接著,我們需要把紅燈的地方修到好,所以要實作一個login函數,用以檢核帳號密碼,最終回傳成功或失敗

1
2
3
4
//============== Code ==================
func login(acc:String,pass:String)->Bool{
return true
}

此時跑一下Test, 你會得到login回傳true,喔耶~~ 測試通過了,此時成功的狀態(success),我們簡稱為「綠燈」。

身為工程師的你,摸一下你的xx,不用多想也知道,不管什麼東西進來都是true,所以,我們白痴地依依 Test Case 來重構一下功能

1
2
3
4
5
6
7
8
//============== Code ==================
func login(acc:String,pass:String)->Bool{
if acc=="" || pass=="" {
return false
}else{
return true
}
}

白痴重構結束後,再跑一下測試,嗯嗯嗯 還是綠燈。那就可以拿下一個Test Case來做了


第二個使用者故事 如下
1
2
3
假設(Given),存在有一個可用的帳號與密碼
當(When),使用者開啟APP,輸入帳號 aki ,密碼 123,點選「登入」
然後(Then),要跳出錯誤訊息(Failed)

第二個Test case 如下:

寫測試

1
2
3
func testLoginFailed() {
XCTAssertFalse(login(acc: "aki", pass: "123"))
}

跑測試亮紅燈

改code

1
2
3
4
5
6
7
8
9
10
11
12
13
//============== Code ==================
func login(acc:String,pass:String)->Bool{
if acc=="aki" {
if(pass=="qaz"){
return true
}else if pass=="123"{
return false
}
return false
}else{
return false
}
}

跑一下測試,綠燈

重構,跑一下測試,綠燈

1
2
3
4
5
6
7
8
9
10
11
//============== Code ==================
func login(acc:String,pass:String)->Bool{
if acc=="aki" {
if(pass=="qaz"){
return true
}
return false
}else{
return false
}
}

無限loop直到所有的測試都通過,此時程式就寫好了,而且應該被完整的重構過。

反思一下我們在上面的所有動作

我們把上述的動作抽離出來

1
2
3
Action 01: 寫好 Test Case 後,跑一下測試(嗯。。。紅燈)
Action 02: 改code之後,跑一下測試(嗯。。。綠燈)
Action 03: code的有沒有要重構或調整的部份,改完之後再跑一次測試,直到綠燈(重構)

在01-03之間,藉由不斷地添加Test Case,跑test case,寫程式,重構,來建構一個開發的流程。

這就是 TDD 呀!!

想一下,不寫測試不行嗎?

可以啊

那有可能會發生什麼問題?

QA會花很多力氣在做局部的測試,當一個專案愈大,就愈難完整的測到完,沒測試到的部份,頂多就是閉著眼睛上線,直接讓User幫你測試。這部份留到QA的部份再加以解釋

先寫代碼,再寫測試不行嗎?

可以啊

後來寫的測試可以證明這個代碼沒有bug,但是不能證明這個代碼能夠解決User使用上的問題

舉個例子吧。

我們想要寫一個function來知道一個字串是不是數字

比如說 “123” 時為true,”ABC”時為false,

於是工程師寫下了一段Code,

1
2
3
4
5
6
7
8
func isInteger(aStr:String) -> Bool{
do {
let a = try Int(aStr)
return true
}catch {
return false
}
}

語意是利用變數型別的轉換正確或失敗來判斷這一個變數是不是數字

如果不做測試的話,其實是不會知道問題在那裡

如果輸入了 “123” 為 true, 但是輸入了 “ABC” 一樣是true

1
2
3
4
5
6
7
8
9
10
func isInteger(aStr:String) -> Bool{

let number = Int(aStr)

if number != nil {
return true
}

return false
}

兩個 Test Case “123”,”ABC” 的回傳都符合我們的預期。

在一般的情況下,也許上面的code沒有問題,但是如果今天也許我們需要經營辛巴威市場時,我們加個測試看會不會過

“1234567890123456789012345678901234567890”?

1
2
3
func testIsTrue(){
XCTAssert(isInteger(aStr: "1234567890123456789012345678901234567890"))
}

再次強調,在很多情況之下,我們是很相信工程師的

工程師有沒有可能出錯? 那怕是千萬之一的機會,產品如果有問題,公司、團隊、個人能不能承擔犯錯之後的結果?

工程師能不能信用擔保,或者用性命擔保程式沒有什麼?

寫了測試就不會出錯了嗎?

會,人還是會犯錯的,只是在一連串的犯錯的學習中,可以透過TestCase來了解,程式為什麼這樣子寫
(有時候,別人也可以透過Test Case來了解程式的流程而不用深入程式實作的細節)

應不應該追求所謂的100%的測試覆蓋率(100 Test Coverage)?

要看投入的時間成本,和所回收的效益,才能判斷值不值得。這個判斷的點,我們晚點回過頭來探討。

註1:如果你想要做TDD的練習,那麼有一些道場(dojo、どじょ)提供一些套路(Kata、型、かた),其中一個比較有名的道場如下:
http://www.codingdojo.org/KataCatalogue/

註2:
辛巴威幣


想一下: 有一段正則表示式是,你敢不敢直接把code推上production?
1
\A(?=[[email protected]!#$%&'*+/=?^_`{|}~-]{6,254}\z)(?=[a-z0-9.!#$%&'*+/=?^_`{|}~-]{1,64}@)[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:(?=[a-z0-9-]{1,63}\.)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?=[a-z0-9-]{1,63}\z)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z