在測試驅動開發(TDD)總結——原理篇一文中已經對 TDD 作了概念性總結。而我的以爲理論知識的缺點在於它只強調外部刺激而缺少學習者的內部心理過程,好比很難基於已有的經驗對理論性知識創建映射關係,所以客觀的實踐纔是檢驗真理的惟一標準。奔着這個目標,這些天花了一些時間去選擇案例,由於好的故事或者好的案例既能讓本身更有代入感,也能發揮 TDD 的魅力。java
文章包括了案例,任務分解,報數部分的接口設計,單元測試命名規則和 TDD 案例實踐,關於文章的源碼已經放到個人我的 Github,但願接下來的 TDD 實踐系列文章對讀者也有一些收穫。git
TDD (Test Driven Development) 在不一樣的圈子、不一樣的角色的認知中可能會有不一樣的理解,有人可能會理解成 ATDD(Acceptance Test Driven Development),也有人可能會理解成 UTDD(Unit Test Driven Development),爲了不產生歧義,文章涉及到 TDD 專指 UTDD(Unit Test Driven Development),即 「單元測試驅動開發」。github
你是一名體育老師,在某次課距離下課還有五分鐘時,你決定搞一個遊戲。此時有 100 名學生在上課。遊戲的規則是:算法
如今,咱們須要你完成一個程序來模擬這個遊戲,它首先接受 3 個特殊數,而後輸出 100 名學生應該報數的數或單詞。好比:微信
輸入:框架
3,5,7工具
輸出(片斷):單元測試
1,2,Fizz,4,Buzz,Fizz,Whizz,8,Fizz,Buzz,11,Fizz,Fizz,Whizz,FizzBuzz,16,17,Fizz,19,Buzz,…,100學習
在 TDD 以前進行需求分析能夠在一開始就明確完成任務的目標是什麼,以便於減小理解誤差所帶來的低級錯誤;緊接着對需求進行任務分解,目的是獲得一份能夠被驗證的任務清單。在實踐 TDD 的過程可能還會調整任務清單,好比添加新的任務,或者刪除掉冗餘的任務等等。測試
在對需求進行分析的過程當中,我會先從參與者的角度分析整個案例涉及到的角色有哪些,發現有兩種角色參與到遊戲中,分別是老師和學生;而後再從職責的角度分析得出老師的職責是發起遊戲、定義遊戲規則和說出 3 個不重複的個位數數字;學生的職責是參與遊戲並根據遊戲規則報數。最終我初步得出如下任務清單:
我發現「學生報數」任務受到一系列遊戲規則的影響,所以我會對該任務進行細化,而且尋找該任務的特殊需求,以便於對任務作必定程度的規劃。在分析的過程當中,對於比較特殊或者比較重要的規則我會作好標註,避免遺忘。下面是我細分後的任務清單:
有時候分解出來的任務沒有體現出優先級和依賴關係,爲了提升工做效率,須要對任務進行規劃,讓事情變得更加有條理,避免在一堆任務中迷失方向。
因爲案例難度通常,因此這一步能發揮的空間不大,不過在這一步能夠思考應該從哪一個任務開始,選擇的標準能夠參照這三個:
經過判斷任務是不是主要流程來判斷任務的重要程度,好比「驗證入參」的重要程序相對「學生報數」較低,能夠晚點作。
經過分析任務的依賴關係來識別任務的前後順序,具體優先級因人而異,有人喜歡採用自頂向下,有人喜歡採用自底向上。好在 Mock 能夠幫助開發人員隔離依賴,還能夠經過 Mock 的方式驅動出類和接口而不依賴於具體實現,避免陷入尋找任務先後順序的煩惱中。
分析任務的難度須要經過需求分析和經驗得出,經過分析上面的案例能夠知道難點在於學生報數的算法上。對於我我的來講,除非是核心任務,不然我不會一開始就選擇難度大的非核心任務做爲開始任務。
經過分析,恰好難度較大的任務「學生報數」是整個遊戲的核心功能,最終我選擇先作這個任務。
should_xxx_when_xxx
,例如:should_return_false_when_1_is_greater_than_2
。在編寫測試方法時,應該遵循 Given-When-Then 模式(在給定xx狀況下,當作了xx操做,會獲得xx反饋)這種模式可讓開發人員專一併思考如下這幾件事情:
得益於 BDD 思想和工具,這種命名方法是我在 BDD 的實踐過程當中琢磨出來的(固然不止我在用這種命名規則),它包含但不只限於如下優勢:
到如今需求已經明確,測試命名規範已擬定,任務已敲定,能夠開始 TDD 了。
早期的開發習慣(編碼-運行-觀察)會致使開發人員過早陷入實現細節,這種開發習慣的缺陷之一在於反饋週期長,不利於小步快跑的節奏,因此在實踐 TDD 的過程當中須要時刻提醒本身 TDD 的口號和規則,培養本身養成新的思惟習慣。
敲定任務
根據 TDD 的總體流程,此時須要想一下我要作什麼,想一想如何測試它,而後寫一個小測試。思考所需的類、接口、輸入和輸出。
根據以前的需求分析,學生須要明確本身對應的序號和遊戲規則才能進行報數,所以驅動出 Student
類和 String countOff(position, gameRules)
方法,觀察 countOff
方法發現須要用到遊戲規則,因此還驅動出 GameRule
類。
編寫足夠的代碼使測試失敗(明確失敗總比模模糊糊的感受要好)。
@Test
public void should_return_fizz_when_just_a_multiple_of_the_first_number() {
List<GameRule> gameRules = new ArrayList<>();
assertThat(Student.countOff(3, gameRules)).isEqualTo("Fizz");
}
複製代碼
這段代碼運行的時候編譯不經過,是由於缺乏了必要的類和方法,因此我很快地補上了如下代碼:
public class Student {
public static String countOff(Integer position, List<GameRule> gameRules) {
return "";
}
}
public class GameRule {
}
複製代碼
而後運行了單元測試獲得如下錯誤消息:
編寫剛恰好使測試經過的代碼(保證以前編寫的測試也須要經過)。
檢查完錯誤後,我發現加入了GameRule
影響了我對代碼明顯實現的判斷,因此此時我使用僞實現策略使測試儘快經過,以便於在持續細微的反饋中捕獲明顯實現,所以我很快鍵入如下僞代碼:
public static String countOff(Integer position, List<GameRule> gameRules) {
return "Fizz";
}
複製代碼
謝天謝地測試經過了,很是快就獲得了我想要的結果:
我知道這段代碼是有問題的,如今我在思考是繼續編寫GameRule
使僞實現變成明顯實現?仍是挑下一個任務作並把「編寫GameRule
」記錄到任務清單等以後再去作呢?這個選擇的標準很簡單,就是判斷完成這個任務須要花多長時間,若是很快就能作完,那就繼續作,若是須要花上一段時間,那就記下來跳下一個任務。經過個人分析,只須要給 GameRule
增長兩個成員變量(數字和對應的術語)就能夠達到個人目標, 而後我調整了對應的測試代碼:
@Test
public void should_return_fizz_when_just_a_multiple_of_the_first_umbe() {
List<GameRule> gameRules = Lists.list(
new GameRule(3, "Fizz"),
new GameRule(5, "Buzz"),
new GameRule(7, "Whizz")
);
assertThat(Student.countOff(3, gameRules)).isEqualTo("Fizz");
}
複製代碼
緊接着增長了如下代碼:
public class GameRule {
private Integer number;
private String term;
public GameRule(Integer number, String term) {
this.number = number;
this.term = term;
}
...
}
public class Student {
public static String countOff(Integer position, List<GameRule> gameRules) {
if (position % gameRules.get(0).getNumber() == 0) {
return gameRules.get(0).getTerm();
}
return position.toString();
}
}
複製代碼
而後運行測試:
由於目前測試和代碼量不多,也沒有明顯的壞味道,因此暫時不須要重構,直接把當前子任務劃掉並挑下個子任務。因爲文章邊幅有限,在重複屢次 TDD 的總體流程後來到:
public class StudentTest {
private final List<GameRule> gameRules = Lists.list(
new GameRule(3, "Fizz"),
new GameRule(5, "Buzz"),
new GameRule(7, "Whizz")
);
@Test
public void should_return_1_when_mismatch_any_number() {
assertThat(Student.countOff(1, gameRules)).isEqualTo("1");
}
@Test
public void should_return_fizz_when_just_a_multiple_of_the_first_number() {
assertThat(Student.countOff(3, gameRules)).isEqualTo("Fizz");
assertThat(Student.countOff(6, gameRules)).isEqualTo("Fizz");
}
@Test
public void should_return_buzz_when_just_a_multiple_of_the_second_number() {
assertThat(Student.countOff(5, gameRules)).isEqualTo("Buzz");
assertThat(Student.countOff(10, gameRules)).isEqualTo("Buzz");
}
@Test
public void should_return_whizz_when_just_a_multiple_of_the_third_number() {
assertThat(Student.countOff(7, gameRules)).isEqualTo("Whizz");
assertThat(Student.countOff(14, gameRules)).isEqualTo("Whizz");
}
}
public class Student {
public static String countOff(Integer position, List<GameRule> gameRules) {
if (position % gameRules.get(0).getNumber() == 0) {
return gameRules.get(0).getTerm();
} else if (position % gameRules.get(1).getNumber() == 0) {
return gameRules.get(1).getTerm();
} else if (position % gameRules.get(2).getNumber() == 0) {
return gameRules.get(2).getTerm();
}
return position.toString();
}
}
複製代碼
此時代碼的「壞味道」逐漸展現出來,須要引入重構階段來消除重複設計,讓小步快跑的節奏更加踏實。
歡迎關注個人微信訂閱號,我將會持續輸出更多技術文章,但願咱們能夠互相學習。