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
***
廣告
對於軟體測試以及軟體重構有興趣的鄉民,可以參考泰迪軟體以下兩個課程:
***
友藏內心獨白:終於可以開始重構了。