l

2019年2月27日 星期三

鍛鍊你自己的九陽神功

Feb. 25 23:07~23:34


幾年前在上【單元測試這樣學就會了實作班】的時候有一位學員問Teddy…

學員:我在公司組了〈xUnit Test Patterns: Refactoring Test Code〉讀書會,但是效果不好,還是看不懂書中在講什麼。

Teddy:先撇除英文的因素,我覺得妳們看不懂是很正常的。首先,這本書在講測試,但你們很多測試的基本知識都缺乏,所以看不懂。其次,這本是是用模式(pattern)的方法來講測試,你們也不懂pattern,像是pattern的六大格式,所以讀起書來抓不到重點以及各個pattern之間的關聯性。

***

上禮拜四中午跟學生meeting,指導教授也來參加。我跟他們快速介紹event storming與DDD的aggregate和repository設計,前後大概花了10~15分鐘。指導教授當然是完全聽懂,學生「表面上」也聽懂了,但Teddy猜想實際上學生應該還不太懂,因為這些東西平常Teddy大概至少要花1~2小時來解釋。

在身邊認識的人裡面,大概也只有跟指導教授討論事情,可以享受光纖通訊的高速頻寬。大部分遇到的學生,溝通頻寬都還處在用modem撥接的速度。

***

這個時代大家做事情都在講求快速,但在讀書這件事上面Teddy動作很慢。經典好書,不是買回家之後「假裝看完」就沒事了,而是要看懂。這個過程,絕對不是看過1~2遍那麼簡單,至少看個5~6遍都是很稀鬆平常的事情。

把一個問題徹底弄清楚,有時候需要2~3年的時間的反覆迭代、沈澱、發酵。緣分到了,問題也就想通了。

知識的層次就是這樣一層、一層的累積出來。馬步扎得深,成為日後吸納新知的加速器。電影倚天屠龍記裡面張三豐說:「有九陽神功護體,學什麼功夫都快。」Teddy年輕時第一次聽到這句話,覺得哪有這種事,完全不符合邏輯,鬼扯。

老了之後才發現,這句話是真的XD。

***

友藏內心獨白:九陽神功就是你的學習力。

2019年2月26日 星期二

從領域驅動設計看Tennis Kata(下)

Feb. 25 18:20~19:38

▲人神之間的共通語言XD


Tennis Refactoring Kata介紹

鄉民們可以在這裡找到10幾個語言的Tennis Refactoring Kata程式碼。以Java語言為例,專案結構如下,一共有五個Java檔案:


  • TennisGame:定義TennisGame介面,讓TennisGame1~TennisGame3實作。


  • TennisGame1:第一個版本的重構對象,裡面有幾個很明顯的壞味道(Bad Smell),也有幾個小地方稍微把邏輯整理一下就可以完成重構任務。


  • TennisGame2:第二個版本的重構對象,這個版本的程式碼更長,用最無腦的方式把計分規則逐一展開,想要模擬不太會寫程式的新手所完成的作品。乍看之下有點頭大。但只要做過第一個版本,你已經知道網球遊戲的計分規則。只要把成對的if逐一合併之後,重構就大致完成。


  • TennisGame3:第三個版本的重構對象,這個版本的程式最精簡,只要套用rename與extract method這兩個重構就差不多可以搞定。


  • TennisTest:測試案例,因為這個kata的目的是要練習refactoring,所以範例程式提供了完整的測試案例。鄉民們在練習的時候,只要測試案例通過,就代表沒有改變程式原本的外部行為。


Teddy今天不是要討論如何重構這三個class,重構的活動就交給鄉民們自行練習。

***

和DDD有什麼關係?

▼下列程式為TennisGame2的(一種)重構結果。


鄉民們可以發現,經過重構後計算分數的邏輯分成四個階段:

  • BeforeDeuce:在deuce之前。
  • Deuce:發生Deuce的時候。
  • Advantage:差一分就獲勝的時候。
  • GameOver:這一局結束。

上述程式當然還可以進一步重構,眼尖的鄉民也許發現可以套用State設計模式,把getScore函數簡化成只有一行:

public String getScore() {
         return state.getScore();
     }

這整個過程是一小步、一小步重構之後,如果幸運的話最後走到套用State設計模式,讓自己看起來好棒棒。但是,鄉民們也可以從領域驅動(DDD)設計的角度來思考這個重構。假設鄉民們找到網球專家跟你一起討論需求,從網球專家的互動中,你們整理出以下domain model:

此時你們只關注Game與Game的計分方式。因為Game的計分方式與狀態有關,不同的狀態有不同的分數顯示邏輯。在與領域專家討論時,你們決定把Deuce之前的狀態稱為Initial,其他三個計分狀態分別為Deuce、Advantage、GameOver。

到這邊為止,有兩種可能的方式進行重構:

  • 小步快跑:繼續採用小步快跑的方式,慢慢重構系統,不要直接決定就是要套用State設計模式,除非程式的結構已經很明顯表達出這種可能性。
  • 重構為模式:與領域專家討論過後,你對於問題領域更佳清楚。domain model已經暗示了可以套用State設計模式,於是你決定「用跳的」,直接把程式重構為套用State設計模式。

以下為套用State設計模式後的檔案


套用State設計模式的TennisGame。Teddy把state切換邏輯寫在TennisGame身上,也可以改成放在每一個具體狀態類別身上。

***

結論

用最初淺的方式來看DDD,花點時間整理domain model,並建立ubiquitous language。無論採用小步快跑的重構,直接重構成設計模式,或是採用TDD的方式來開發系統,有一個共同的語言,對於任何方式的軟體開發都很有幫助

***

友藏內心獨白:寫程式有時候不是英文不好不會取名字,而是沒有花時間建立ubiquitous language。

2019年2月25日 星期一

從領域驅動設計看Tennis Kata(上)

從領域驅動設計看Tennis Kata(上)

Feb. 25 12:39~14:00

▲Eiffel正在修練睡夢讀書法Kata


Tennis Kata

Kata,中文翻譯成套路。在敏捷開發領域中,kata指「被設計用來練習特定技巧(特別是程式設計)的題目與參考答案。」

最近Teddy在修改【軟體重構入門實作班】與【單元測試這樣學就會了實作班】教材內容,花點了時間重新練習了幾個kata。今天想從DDD(domain-driven design;領域驅動設計)的觀點,談一個經常被練習的題目:Tennis Kata。

Tennis Kata有兩個版本,一個用來練習TDD,另一個用來練習Refactoring。透過TDD方式撰寫或直接重構已完成的網球比賽計分程式,分別學習這兩種技能。

一個完整的網球比賽分成:

  • 比賽(Match)
  • 盤(Set)
  • 局(Game)

一場比賽有若干盤,一盤有若干局。詳細說明可參考維基百科。為了簡化起見,Tennis Kata只計算局(Game)的結果。關於局的計分方式,維基百科的解釋如下:

網球每局的記分方法:從0至3分分別為「零」(love)、「十五」(fifteen)、「三十」(thirty)和「四十」(forty)。記分時,發球手的得分在前。因此「30比0」的意思是,發球手贏得2分,而接球手還未得分。

當雙方運動員都得到了3分時,一般叫「平局」(deuce而非「40比40」。在出現平局後一名球手再得一分,被稱為「占先」(advantage),而不再記分。如果在占先的情況下失去一分,就再度回到平局;如占先後再得一分,就贏得一局。

今天先談Tennis TDD Kata,明天再談重構。

***

Tennis TDD Kata

網路上有很多Tennis TDD Kata的範例,Google搜尋時Teddy找了第一筆資料來參考。

作者把用TDD完成Tennis Kata的主要過程記錄下來,但是讀到最後完成的程式,Teddy覺得下列的IsDeuce函數的邏輯好像怪怪的。


▲節錄自Day29 TDD套路經典!? Tennis Game!


如果第一個比賽選手的分數大於等於3,IsDeuce回傳true。但根據網球比賽的規則,deuce有兩個條件:

  • 雙方選手的分數都是3分,或
  • 超過3分之後,在這一局比賽結束前,如果再度平手,也稱為deuce

那為什麼這段程式會寫成判斷 _firstPlayerScore >= 3 ,連是否平手都沒判斷?

繼續往前閱讀文章,Teddy發現作者在判斷是否為deuce的時候,production code長成下面這樣。

 

▲節錄自Day29 TDD套路經典!? Tennis Game!

接著作者用extract method重構,把 _firstPlayerScore >= 3 放到IsDeuce函數,所以造成isDeuce的邏輯變成_firstPlayerScore >= 3。

***

和DDD有什麼關係?

DDD強調domain modelubiquitous language(在限定領域中開發人員與利害關係人一致性使用的語言或是術語),幫助開發人員在系統重構時找出重構目標、重新分派物件責任,最後利用重構逐步將ubiquitous language實作在程式碼中(ubiquitous language in code)。上面這個例子,測試案例雖然都通過,程式也可以動,但是並沒有完全做到ubiquitous language in code。因為deuce的定義,包含在Tennis的ubiquitous language裡面,也就是前面提到的:

  • 雙方選手的分數都是3分,或
  • 超過3分之後,在這一局比賽結束前,如果再度平手,也稱為deuce

而上面的程式範例並沒有正確表達這個邏輯。

***

重構的錯誤

就算不考慮ubiquitous language的觀點,直接把_firstPlayerScore >= 3用extract method改成IsDuece也不洽當。為什麼,再看一次作者原本的程式碼:

 

▲節錄自Day29 TDD套路經典!? Tennis Game!

這個程式碼有兩個if,結構為:

if (_firstPlayerScore == _secondPalyerScore) {

   if (_firstPlayerScore >= 3) {

return “Deuce”;

  }

}

也就是說,deuce的條件必須兩個if同時成立原本作者的程式邏輯是對的,反倒是重構之後沒注意到if (_firstPlayerScore >= 3)其實是存在另一個 if之下,直接extract method反倒讓程式碼與領域知識不一致

如果可以把程式改成下面這樣,isDeuce就符合deuce在Tennis領域的定義,應該比較落實ubiquitous language in code的精神。

***

讓程式碼說實話

程式設計師應該大多認同,程式是寫給人看的,不是寫給電腦看的。落實ubiquitous language in code,你的程式碼就會說人話,而且是實話,不是謊話。

如此一來,可以避免程式碼和領域知識不一致。程式比較容易理解,開發人員也比較敢修改程式。這也是一種擁抱改變,讓軟體變軟的方法

***

友藏內心獨白:說實話很重要。

2019年2月20日 星期三

透過測試涵蓋率讓重構更有信心(下):特徵測試

Feb. 19 12:59~15:13

▲不管黑貓白貓,能陪睡的就是好貓XD


重構既存系統的難題

前兩集〈透過測試涵蓋率讓重構更有信心(上):分支涵蓋率〉與〈透過測試涵蓋率讓重構更有信心(中):突變測試〉所使用的範例Account類別的withdraw函數,其程式邏輯很簡單,藉由觀看程式碼很容易就可以設計出足夠的測試案例,以支持後續的重構活動。

在實務上,鄉民們經常要對既存系統(legacy system)進行重構。這些既存系統除了沒有自動化測試案例,通常程式都寫得很亂,且缺乏文件,邏輯複雜導致沒人看得懂程式在做什麼。因此,重構既存系統成為一件很困難的工作。因為沒人看得懂程式邏輯,別說重構,連要幫程式補寫測試案例都十分困難

***

特徵測試

有一種測試技巧稱為特徵測試(Characterization Test,又叫做Golden Master Test),非常適合幫既存系統設計測試案例。

特徵測試的想法很簡單:把既存系統當作一個黑盒子,既然沒人看得懂既存系統的程式邏輯,那就幹脆不要試圖去了解它的內部邏輯。只要想辦法把既存系統的實際外部行為(系統執行結果)記錄下來,這份紀錄下來的資料稱作golden master。接下來就可以去重構系統,每次重構之後重新執行一次程式,把執行結果跟原本錄製下來的golden master比對。如果比對結果一樣,就表示此次重構沒有破壞系統原本的行為。

▼特徵測試與golden master。

***

範例介紹

Teddy找了〈Gilded Rose Kata〉當作既存系統範例,待測程式有兩個類別,分別是Item與GildedRose。

▼Item類別很簡單,沒什麼程式邏輯,就是一個簡單的data class(存放資料的類別)。


▼真正棘手的是GildedRose類別,它只有一個updateQuality函數,但是這個程式邏輯真的是很難了解它的明白。


▼這個範例有一個測試案例如下。很顯然光是只有這個測試案例遠遠不足以讓開發人員有信心去重構程式,但就算是把待測程式原始碼讀了好幾回,依然不知道該怎麼設計其他測試案例。

***

Approval Tests工具簡介

接下來的問題是要怎麼產生足夠的特徵測試,以便得到一份足以支持後續重構活動的golden master。在這裡Teddy要使用Approval Tests工具,它使用資料驅動測試案例(data-driven tests,又稱為參數化測試案例)技巧,將每次測試結果全部紀錄下來當作golden master。只要提供的資料(參數)夠多,測試案例的涵蓋率越高,開發人員對於所產生的golden master信心也越高,便可開始進行重構活動。

▼首先打開〈Gilded Rose Kata〉Java版本的程式範例,將下列相依性加入pom.xml檔案中。


▼接著在updateQuality測試案例中加入Approvals.verify(app.items[0].name),這一行可以用來取代JUnit的assertEqulas。


▼執行測試案例,結果居然是錯的?!


▼打開測試案例目錄,發現增加了兩個檔案:

  • GildedRoseTest.updateQuality.approved.txt
  • GildedRoseTest.updateQuality.received.txt


*approved.txt檔案相當於golden master,*received.txt則是此次執行測試案例所得到的程式輸出結果。Approvals會自動比對這兩個檔案的內容,如果內容相同表示程式的行為沒有被改變,測試案例通過,反之則不通過。*approved.txt檔案如果不存在,Approvals會自動產生一份內容為空白的檔案。


▼把*received.txt(左邊)與*approved.txt(右邊)檔案打開,可以發現左邊的內容為foo,右邊是空白,比對失敗,所以Approvals測試案例執行失敗。


▼要讓Approvals測試成功很簡單,只要把*received.txt改名成*approved.txt。


▼再執行一次測試案例即可。

***

產生Golden Master

Approvals工具有一個CombinationApprovals類別,它提供verifyAllCombinations函數,輸入一個待測程式的函數(closure),以及若干個參數,便可以撰寫資料驅動測試案例,然後將所有測試結果記錄下來。


▼先把原本的測試案例用extract method重構一下,獨立成doUpdateQuality method,以便等一下將它傳給CombinationApprovals


▼原本的updateQulaity測試案例變成這樣:

  • 提供給verifyAllCombinations函數的第一個參數是this::doUpdateQuality,也就是剛剛重構後的測試案例。
  • 因為執行doUpdateQuality需要三個參數,分別是name、sellin、quality,所以接著提供Stirng []、Integer []、Interger [] 給verifyAllCombinations函數。
  • 目前只提供一組資料{”foo”, –1. 0},隨著提供的測試資料越多,Golden Master的內容就越完整。


▼只有一組資料{”foo”, –1. 0}產生的*received.txt檔案內容如下。


▼看一下測試涵蓋率,好多程式碼都沒被執行到,還要多增加幾組測試資料才行。

***

▼套用前兩集提到的分支涵蓋率突變測試技巧,最後的特徵測試如下。


▼已達到100的分支涵蓋率,也通過手動的突變測試。


▼最終的Golden Master內容:

[foo, -1, 0] => foo, -2, 0
[foo, -1, 1] => foo, -2, 0
[foo, -1, 49] => foo, -2, 47
[foo, -1, 50] => foo, -2, 48
[foo, 0, 0] => foo, -1, 0
[foo, 0, 1] => foo, -1, 0
[foo, 0, 49] => foo, -1, 47
[foo, 0, 50] => foo, -1, 48
[foo, 2, 0] => foo, 1, 0
[foo, 2, 1] => foo, 1, 0
[foo, 2, 49] => foo, 1, 48
[foo, 2, 50] => foo, 1, 49
[foo, 6, 0] => foo, 5, 0
[foo, 6, 1] => foo, 5, 0
[foo, 6, 49] => foo, 5, 48
[foo, 6, 50] => foo, 5, 49
[foo, 11, 0] => foo, 10, 0
[foo, 11, 1] => foo, 10, 0
[foo, 11, 49] => foo, 10, 48
[foo, 11, 50] => foo, 10, 49
[Aged Brie, -1, 0] => Aged Brie, -2, 2
[Aged Brie, -1, 1] => Aged Brie, -2, 3
[Aged Brie, -1, 49] => Aged Brie, -2, 50
[Aged Brie, -1, 50] => Aged Brie, -2, 50
[Aged Brie, 0, 0] => Aged Brie, -1, 2
[Aged Brie, 0, 1] => Aged Brie, -1, 3
[Aged Brie, 0, 49] => Aged Brie, -1, 50
[Aged Brie, 0, 50] => Aged Brie, -1, 50
[Aged Brie, 2, 0] => Aged Brie, 1, 1
[Aged Brie, 2, 1] => Aged Brie, 1, 2
[Aged Brie, 2, 49] => Aged Brie, 1, 50
[Aged Brie, 2, 50] => Aged Brie, 1, 50
[Aged Brie, 6, 0] => Aged Brie, 5, 1
[Aged Brie, 6, 1] => Aged Brie, 5, 2
[Aged Brie, 6, 49] => Aged Brie, 5, 50
[Aged Brie, 6, 50] => Aged Brie, 5, 50
[Aged Brie, 11, 0] => Aged Brie, 10, 1
[Aged Brie, 11, 1] => Aged Brie, 10, 2
[Aged Brie, 11, 49] => Aged Brie, 10, 50
[Aged Brie, 11, 50] => Aged Brie, 10, 50
[Backstage passes to a TAFKAL80ETC concert, -1, 0] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, -1, 1] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, -1, 49] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, -1, 50] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 0] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 1] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 49] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 50] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 2, 0] => Backstage passes to a TAFKAL80ETC concert, 1, 3
[Backstage passes to a TAFKAL80ETC concert, 2, 1] => Backstage passes to a TAFKAL80ETC concert, 1, 4
[Backstage passes to a TAFKAL80ETC concert, 2, 49] => Backstage passes to a TAFKAL80ETC concert, 1, 50
[Backstage passes to a TAFKAL80ETC concert, 2, 50] => Backstage passes to a TAFKAL80ETC concert, 1, 50
[Backstage passes to a TAFKAL80ETC concert, 6, 0] => Backstage passes to a TAFKAL80ETC concert, 5, 2
[Backstage passes to a TAFKAL80ETC concert, 6, 1] => Backstage passes to a TAFKAL80ETC concert, 5, 3
[Backstage passes to a TAFKAL80ETC concert, 6, 49] => Backstage passes to a TAFKAL80ETC concert, 5, 50
[Backstage passes to a TAFKAL80ETC concert, 6, 50] => Backstage passes to a TAFKAL80ETC concert, 5, 50
[Backstage passes to a TAFKAL80ETC concert, 11, 0] => Backstage passes to a TAFKAL80ETC concert, 10, 1
[Backstage passes to a TAFKAL80ETC concert, 11, 1] => Backstage passes to a TAFKAL80ETC concert, 10, 2
[Backstage passes to a TAFKAL80ETC concert, 11, 49] => Backstage passes to a TAFKAL80ETC concert, 10, 50
[Backstage passes to a TAFKAL80ETC concert, 11, 50] => Backstage passes to a TAFKAL80ETC concert, 10, 50
[Sulfuras, Hand of Ragnaros, -1, 0] => Sulfuras, Hand of Ragnaros, -1, 0
[Sulfuras, Hand of Ragnaros, -1, 1] => Sulfuras, Hand of Ragnaros, -1, 1
[Sulfuras, Hand of Ragnaros, -1, 49] => Sulfuras, Hand of Ragnaros, -1, 49
[Sulfuras, Hand of Ragnaros, -1, 50] => Sulfuras, Hand of Ragnaros, -1, 50
[Sulfuras, Hand of Ragnaros, 0, 0] => Sulfuras, Hand of Ragnaros, 0, 0
[Sulfuras, Hand of Ragnaros, 0, 1] => Sulfuras, Hand of Ragnaros, 0, 1
[Sulfuras, Hand of Ragnaros, 0, 49] => Sulfuras, Hand of Ragnaros, 0, 49
[Sulfuras, Hand of Ragnaros, 0, 50] => Sulfuras, Hand of Ragnaros, 0, 50
[Sulfuras, Hand of Ragnaros, 2, 0] => Sulfuras, Hand of Ragnaros, 2, 0
[Sulfuras, Hand of Ragnaros, 2, 1] => Sulfuras, Hand of Ragnaros, 2, 1
[Sulfuras, Hand of Ragnaros, 2, 49] => Sulfuras, Hand of Ragnaros, 2, 49
[Sulfuras, Hand of Ragnaros, 2, 50] => Sulfuras, Hand of Ragnaros, 2, 50
[Sulfuras, Hand of Ragnaros, 6, 0] => Sulfuras, Hand of Ragnaros, 6, 0
[Sulfuras, Hand of Ragnaros, 6, 1] => Sulfuras, Hand of Ragnaros, 6, 1
[Sulfuras, Hand of Ragnaros, 6, 49] => Sulfuras, Hand of Ragnaros, 6, 49
[Sulfuras, Hand of Ragnaros, 6, 50] => Sulfuras, Hand of Ragnaros, 6, 50
[Sulfuras, Hand of Ragnaros, 11, 0] => Sulfuras, Hand of Ragnaros, 11, 0
[Sulfuras, Hand of Ragnaros, 11, 1] => Sulfuras, Hand of Ragnaros, 11, 1
[Sulfuras, Hand of Ragnaros, 11, 49] => Sulfuras, Hand of Ragnaros, 11, 49
[Sulfuras, Hand of Ragnaros, 11, 50] => Sulfuras, Hand of Ragnaros, 11, 50

***

廣告

對於軟體測試以及軟體重構有興趣的鄉民,可以參考泰迪軟體以下兩個課程:

***

友藏內心獨白:終於可以開始重構了。

2019年2月19日 星期二

透過測試涵蓋率讓重構更有信心(中):突變測試

Feb. 18 23:47 ~ Feb. 19 00:39

▲Ada:切換為超人模式XD


突變測試

如果測試案例涵蓋足夠多的待測程式(System Under Test;SUT)行為,當待測程式被改變時,重新執行所有的測試案例其結果應該失敗。這代表測試案例捕捉到不正確的程式行為(與測試案例所記載的程式行為不符),突變測試(Mutation Testing)的想法就是從此而來—藉由改變待測程式的邏輯,驗證測試案例是否足夠。

***

例子

▼首先手動把withdraw程式的 amount > 0 改成 amount >1,然後重新執行測試案例。如果測試案例涵蓋率足夠多,其執行結果應該是失敗的(因為此時待測程式的行為被更改和原本測試案例所記錄的行為不同)。


▼執行結果居然是綠燈,原本三個測試案例都通過,也就代表原本的測試案例有遺漏了程式的行為,趕快補寫測試案例吧。


▼先把原本withdraw函數的程式碼復原成突變前的正常版本,然後增加一個單元測試來涵蓋剛剛程式碼突變後所沒捕捉到的行為。


▼重新執行全部四個單元測試案例,成功。


▼這時候在手動把withdraw程式的 amount > 0 改成 amount >1。


▼重新執行四個測試案例,執行結果失敗,代表程式的突變行為被測試案例抓到了。換句話說,透過突變測試技巧,進一步增強測試案例的涵蓋度

***


更多的突變

▼再看一次withdraw的程式碼,鄉民們可以試著把 amount > 0 改成 amount == 0、amount < 0,或是把 balance >= amount 改成「小於等於」或是「等於」,來驗證測試案例是否足夠。

以這個例子來看,上述各種突變狀況都可以被四個測試案例給找出來。這時候鄉民們就有相當高的信心,可以開始動手重構,因為「安全網」(足夠的測試案例)已經準備好了。

***


下集預告

withdraw函數的程式邏輯很簡單,就算沒有用突變測試技巧,也可以用傳統的邊界測試(boundary test)技巧來設計測試案例。

下一集將介紹特徵測試(Characterization Test)技巧與Approval Tests工具,針對程式邏輯複雜到爆掉且沒有人看得懂的函數,產生足夠多的測試案例來支援後續重構活動

***

廣告

對於軟體測試以及軟體重構有興趣的鄉民,可以參考泰迪軟體以下兩個課程:

***

友藏內心獨白:疫苗接踵次數要足夠才能有完整的抵抗力。

2019年2月18日 星期一

透過測試涵蓋率讓重構更有信心(上):分支涵蓋率

Feb. 18 21:49~22:50

▲很多開發環境都內建測試涵蓋率工具,上圖為Eclipse


如何定義程式的行為?

重構(refactoring)是在不改變軟體外在行為的前提下,改變內部結構以改善設計品質。但是問題來了,你怎麼知道你在重構的過程中沒有改變軟體的行為?這個問題,更廣義一點來說,就是你要如何定義軟體的行為?

學過程式語言的人都知道,程式語言有「語法」和「語意」。語法定義結構,語意定義行為。語法錯誤可藉由編譯器(Compiler)幫忙找出,所以開發人員並不會害怕語法錯誤。

但是,整個程式的行為(語意)是由撰寫程式的人所定義,一般來說編譯器是無從得知開發人員的意圖。在開發人員修改程式的過程中,如果不小心改壞掉,原本可以動的程式不能動了,或是出現未被查知的bug,那就很麻煩了。

正規程式語言定義語意的方法過於抽象,大部分的開發人員無法直接運用,退而求其次,實務上開發人員通常透過撰寫測試案例來描述或紀錄程式的行為

***

測試案例足夠嗎?

透過測試案例採用列舉(舉例子)的方式來描述程式行為,就衍生出另一個問題:你怎麼知道你的測試案例足夠多到可以把程式的(主要)行為都表達出來?在傳統軟體測試領域,這個問題叫做測試案例足夠性條件(test case adequacy criteria),常見的方式是透過測試涵蓋率來判別測試案例是否足夠。

今天不是要討論測試,而是要借用測試涵蓋率的觀念,幫助開發人員在重構前判斷測試案例是否足夠支持後續的重構行為。

***

例子

▼你有一個Account物件用來代表客戶的銀行帳戶,你想要重構withdraw函數,但是目前沒有任何測試案例。雖然withdraw程式碼只有短短幾行,但你還是有點害怕萬一重構了之後程式有問題怎麼辦。


▼因此在重構前你準備補寫測試案例,你很快地設計了以下兩個測試案例:


▼接著你觀察withdraw函數的測試涵蓋率,發現第19行分支(branch)呈現黃色,表示還沒被100%涵蓋。


▼於是你繼續增加一個新的測試案例。


▼此時withdraw函數的程式碼全部變成綠色,代表達到100%的分支涵蓋率。

***

下集預告

達到100%分支涵蓋率並不代表測試案例通過程式就沒有bug,換句話說並不表示程式的所有行為都已被測試案例所記錄下來。但這至少是一個在實務上可行且有一定代表意義的涵蓋率,可以當作預備開始重構前的第一個小目標。

下一集將介紹突變測試(Mutation Testing)技巧,可以進一步驗證測試案例的有效性。

***

廣告

對於軟體測試以及軟體重構有興趣的鄉民,可以參考泰迪軟體以下兩個課程:

***

友藏內心獨白:學習測試涵蓋率真的有用。