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)技巧,可以進一步驗證測試案例的有效性。

***

廣告

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

***

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

2019年2月13日 星期三

Scrum團隊如何落實自動化測試與持續整合?

Feb. 13 15:25~16:26


問題

朋友的Scrum團隊跑了半年,每個sprint結束所產生的可執行軟體離「潛在可釋出」還有一大段距離(品質不是很好)。團隊開始思考要如何提升品質,讓每個sprint可以交付「潛在可釋出的產品增量」。

團隊從QA部門借調一位測試人員—QA甲,希望借助他的力量提升軟體品質。但是QA甲以往都是用人工手動測試來驗證功能的正確性,並不會撰寫自動化測試案例。因此,QA甲需要等待團隊成員完成user story之後才可以開始測試。但通常user story完成後也接近sprint尾聲,沒剩多少時間讓QA甲去手動測試。

有團隊成員建議,請QA甲在下個sprint測試上個sprint的功能,但是這又引發出其他的問題,例如:

  • 程式碼凍結:QA甲要測試上個sprint的功能,就必須要求開發團隊不要修改上個sprint所完成的功能。實務上並不可行,因為下個sprint很有可能就是要基於上個sprint所完成的功能繼續開發,要求程式碼凍結會讓開發流程過於僵硬。
  • Done Done:因為上個sprint完成的功能留到下個sprint才驗收,那到底上個sprint做完的功能真的有做完嗎?換句花說,上個sprint的Done不是真的Done,還要經過下個sprint的驗證後才知道是不適真的Done。那如果下個sprint找出上個sprint的bug,又該在何時處理?如此一來,沒完沒了,整個開發時程也不容易掌握。

***

理想狀況

自動化測試與持續整合是軟體開發(不管是否採用敏捷)不可分割的一部分,團隊一開始在撰寫功能之前,就要先把持續整合環境架設好,讓自己的空專案可以在持續整合系統上建構,之後才逐一開發新功能。而每一個新功能,都要有足夠的自動化單元測試、整合測試與驗收測試。

為了達到測試自動化的目的,在Scrum團隊中的QA人員(Scrum團隊成員通稱為開發人員)需要具備撰寫自動化測試案例的能力。如果負責測試的開發人員沒有撰寫自動化測試案例的能力,就要考慮:

  • 是否願意學習新技能,並且與開發人員一起緊密合作
  • 考慮離開Scrum團隊

Teddy的意思並不是說所有的測試案例都必須要自動化,有些測試案例,例如UX方面的測試,或是自動化成本太高的UAT,或是探索式測試,還是可以用人工的方式來做。但大原則是絕大部分的測試必須要自動化,而身處Scrum團隊的測試人員也必須具備撰寫自動化測試案例的能力。

***

非理想狀況怎麼辦

如果團隊已經跑Scrum一陣子,一開始沒有落實自動化單元測試與持續整合,要怎麼「半路出家」?

假設團隊中沒有任何一個人具備自動化測試與持續整合個觀念,最快的方式就是請人教,或是去上課,把基本的知識在短時間先補齊。

接下來可以在DoD(Definition of Done)中訂定自動化驗收測試的標準,讓往後所開發的功能都有基本的自動化驗收測試。至於之前所開發的功能,可以採用以下兩種方式來補寫自動化測試:

  • Bug-driven:當發生bug的時候,先寫一個自動化測試案例來反映這個bug。此時這個測試案例一地會失敗,然後去修改程式碼,直到測試案例通過就知道bug改好了。
  • 預算制度:每個sprint投資固定時間去補寫之前的自動化測試案例。

***

測試先行或測試後行?

有些技術能力比較好的團隊,會採用測試先行(test first),也就是測試驅動開發(test-driven development)、行為驅動開發(behavior-driven development)、實例化規格(specification by example)的方式,讓撰寫測試案例成為開發流程中的上游,test code先於production code,以避免測試後行(test last)的缺點—先寫production code,永遠沒有時間寫test code。

Teddy覺得測試先行或是後行都可以,但重點是「測試一定要行」。只要有紀律,養成測試是開發不可分割的一部分,測試先行或測試後行可以依據團隊的能力與專案特性自行調整。

***

友藏內心獨白:自動化才能提高QA的價值。

2019年2月1日 星期五

繞過去,好嗎?

Jan. 31 23:07~23:58

▲逢山開路,遇水搭橋


問題

從去年開始Teddy和指導教授一起帶幾個實驗室學生做研究,目前遵循clean architecture的方法正在開發看板軟體。一個多禮拜前學生問Teddy關於Repository的介面設計,經過討論後Teddy請他們做點研究,下次開會再談。

今天回學校和學生開sprint review會議,順便請學生說明他們研究Repository的結果。學生告訴Teddy他們發現有兩種Repository的設計方法,第一種是接受由外部給定的specification當作查詢條件,可以比較有彈性的支援多種查詢條件。第二種是在Repository介面上設計常見的方法,例如findAll、findById、findChildrenById等。

學生採用第二種設計方法,聽了請他們選擇的理由後,Teddy還是覺得不滿意(forces沒有被平衡XD)。於是請學生把domain model叫出來,一起讀過一次,發現他們所採用的Repository介面設計無法支援domain model的所有物件。

***

程式可以動啊

學生寫的程式可以動,這個sprint所完成的user story還算做得不錯。但是,如果細看軟體設計,其實還有不少可精進之處。

Teddy告訴他們:

Repository的問題還沒徹底解決,雖然程式可以動,但是如果不管這個設計問題,繞過去,以後你們還是有很大的機會會再遇到它。你們是研究生,專案中遇到問題應該利用機會把問題搞懂,徹底解決。日後遇到同樣的問題,既使context不同,別人花三天,你們只要花一小時就可以解決。層次與專業就是這樣累積出來的。

這個問題,最好的解決方式就是你們搞懂後做出設計的決定,然後說服我(教我)。次一等的解決方式,就是我弄懂後教你們。最糟的方式就是不管它,反正程式可以動就好。

***

Repository Pattern

因為Google太方便,所以學生遇到問題尋找資料,大多下個查詢條件,然後看最前面的幾個連結之後就做出結論,草草結束。Teddy在〈增進學習力的三個練習〉提到,首先要知道名詞的定義。學生只知道可以套用Repository pattern,但很可能對於該pattern的定義只是一知半解。

在《Patterns of Enterprise Application Architecture》的第322頁就介紹Repository pattern。在《Domain-Driven Design: Tacking Complexity in the Heart of Software》以及《Implementing Domain-Driven Design》也都有提到,後者並且包含範例程式碼與實作細節。

***

龜毛

大家都說日本人做事很龜毛。同樣的商品,例如Toyota汽車,MIT和MIJ,相信大部分的人還是比較喜歡日本原裝進口。為什麼?因為組裝的人不一樣啊…Orz

軟體開發是一種專業,就好像醫生也是一種專業。你總不希望醫生開刀的時候遇到問題「繞過去」吧?!反正 程式可以動 人還有呼吸就好XD。 

***

友藏內心獨白:書還是要讀,不能只看網路文章。