TDD 實踐-FizzFuzzWhizz(一)

測試驅動開發(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

準備

  1. 理解 OOP。
  2. 瞭解 Java 8。
  3. 熟悉 Intellij IDEA。
  4. 熟悉 TDD 理論性知識,能夠參考 測試驅動開發(TDD)總結——原理篇
  5. 瞭解 Google 輕量級依賴注入框架 Guice 。
  6. 熟悉測試工具 Junit 和 Mockito 的使用。
  7. 熟悉搭建自動化單元測試環境。

案例

你是一名體育老師,在某次課距離下課還有五分鐘時,你決定搞一個遊戲。此時有 100 名學生在上課。遊戲的規則是:算法

  1. 你首先說出三個不一樣的特殊數,要求必須是個位數,好比 三、五、7。
  2. 讓全部學生排成一隊,而後按順序報數。
  3. 學生報數時,若是所報數字是第一個特殊數(3)的倍數,那麼不能說該數字,而要說 Fizz;若是所報數字是第二個特殊數(5)的倍數,那麼要說 Buzz;若是所報數字是第三個特殊數(7)的倍數,那麼要說 Whizz。
  4. 學生報數時,若是所報數字同時是兩個特殊數的倍數狀況下,也要特殊處理,好比第一個特殊數和第二個特殊數的倍數,那麼不能說該數字,而是要說 FizzBuzz, 以此類推。若是同時是三個特殊數的倍數,那麼要說 FizzBuzzWhizz。
  5. 學生報數時,若是所報數字包含了第一個特殊數,那麼也不能說該數字,而是要說相應的單詞,好比本例中第一個特殊數是 3,那麼要報 13 的同窗應該說 Fizz。若是數字中包含了第一個特殊數,那麼忽略規則 3 和規則 4,好比要報 35 的同窗只報 Fizz,不報 BuzzWhizz。

如今,咱們須要你完成一個程序來模擬這個遊戲,它首先接受 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 個不重複的個位數數字;學生的職責是參與遊戲並根據遊戲規則報數。最終我初步得出如下任務清單:

  1. 發起遊戲。
  2. 定義遊戲規則。
  3. 說出 3 個不重複的個位數數字。
  4. 學生報數。
  5. 驗證入參。

任務細分

我發現「學生報數」任務受到一系列遊戲規則的影響,所以我會對該任務進行細化,而且尋找該任務的特殊需求,以便於對任務作必定程度的規劃。在分析的過程當中,對於比較特殊或者比較重要的規則我會作好標註,避免遺忘。下面是我細分後的任務清單:

  1. 發起遊戲。
  2. 定義遊戲規則。
  3. 說出 3 個不重複的個位數數字。
  4. !!! 學生報數。
    • 若是是第一個特殊數字的倍數,就報 Fizz。
    • 若是是第二個特殊數字的倍數,就報 Buzz。
    • 若是是第三個特殊數字的倍數,就報 Whizz。
    • 若是同時是多個特殊數字的倍數,須要按特殊數字的順序把對應的單詞拼接起來再報出,好比 FizzBuzz、BuzzWhizz、FizzBuzzWhizz。
    • 若是包含第一個特殊數字,只報 Fizz (忽略規則 一、二、三、4)
    • 若是不是特殊數字的倍數,而且不包含第一個特殊數字,就報對應的序號。
  5. 驗證入參。

任務規劃

有時候分解出來的任務沒有體現出優先級和依賴關係,爲了提升工做效率,須要對任務進行規劃,讓事情變得更加有條理,避免在一堆任務中迷失方向。

因爲案例難度通常,因此這一步能發揮的空間不大,不過在這一步能夠思考應該從哪一個任務開始,選擇的標準能夠參照這三個:

  • 任務的重要程度
  • 任務的依賴關係
  • 任務的難度

經過判斷任務是不是主要流程來判斷任務的重要程度,好比「驗證入參」的重要程序相對「學生報數」較低,能夠晚點作。

經過分析任務的依賴關係來識別任務的前後順序,具體優先級因人而異,有人喜歡採用自頂向下,有人喜歡採用自底向上。好在 Mock 能夠幫助開發人員隔離依賴,還能夠經過 Mock 的方式驅動出類和接口而不依賴於具體實現,避免陷入尋找任務先後順序的煩惱中。

分析任務的難度須要經過需求分析和經驗得出,經過分析上面的案例能夠知道難點在於學生報數的算法上。對於我我的來講,除非是核心任務,不然我不會一開始就選擇難度大的非核心任務做爲開始任務。

敲定任務

經過分析,恰好難度較大的任務「學生報數」是整個遊戲的核心功能,最終我選擇先作這個任務。

測試命名規範

  1. 測試類以 XXXTest 命名.
  2. 測試方法命名必須採用should_xxx_when_xxx,例如:should_return_false_when_1_is_greater_than_2
  3. 測試方法的代碼邏輯遵循 Given-When-Then 模式。

知識:Given-When-Then

在編寫測試方法時,應該遵循 Given-When-Then 模式(在給定xx狀況下,當作了xx操做,會獲得xx反饋)這種模式可讓開發人員專一併思考如下這幾件事情:

  • Given:驅動咱們思考這個測試是在一個怎樣的上下文中,用到哪些對象,以便於思考須要建立哪些上下文和對象。
  • When:驅動咱們站在用戶的角度去思考這個行爲是什麼,它有哪些輸入,以便於思考方法的命名和入參。
  • Then:驅動咱們思考行爲的反饋是什麼,以便於思考方法的返回值。

思考:測試方法採用 should_xxx_when_xxx 的意義是什麼?

得益於 BDD 思想和工具,這種命名方法是我在 BDD 的實踐過程當中琢磨出來的(固然不止我在用這種命名規則),它包含但不只限於如下優勢:

  • 能夠在把關注點放到行爲上,避免陷入實現的細節中。
  • 命名接近天然語言,表達意圖清晰,可讀性高,受益人羣廣。
  • 很好地控制測試的範圍,大到用戶行爲(偏 BDD),小到邏輯分支(偏 TDD)。

到如今需求已經明確,測試命名規範已擬定,任務已敲定,能夠開始 TDD 了。

常見錯誤

早期的開發習慣(編碼-運行-觀察)會致使開發人員過早陷入實現細節,這種開發習慣的缺陷之一在於反饋週期長,不利於小步快跑的節奏,因此在實踐 TDD 的過程當中須要時刻提醒本身 TDD 的口號和規則,培養本身養成新的思惟習慣。

測試驅動開發

敲定任務

  • 學生報數。
    • 若是是第一個特殊數字的倍數,就報 Fizz。
    • 若是是第二個特殊數字的倍數,就報 Buzz。
    • 若是是第三個特殊數字的倍數,就報 Whizz。
    • 若是同時是多個特殊數字的倍數,須要按特殊數字的順序把對應的單詞拼接起來再報出,好比 FizzBuzz、BuzzWhizz、FizzBuzzWhizz。
    • 若是包含第一個特殊數字,只報 Fizz (忽略規則 一、二、三、4)
    • 若是不是特殊數字的倍數,也不包含第一個特殊數字,就報 Fizz。

根據 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 的總體流程後來到:

  • 學生報數。
    • 若是是第一個特殊數字的倍數,就報 Fizz。
    • 若是是第二個特殊數字的倍數,就報 Buzz。
    • 若是是第三個特殊數字的倍數,就報 Whizz (當前任務)
    • 若是同時是多個特殊數字的倍數,須要按特殊數字的順序把對應的單詞拼接起來再報出,好比 FizzBuzz、BuzzWhizz、FizzBuzzWhizz。
    • 若是包含第一個特殊數字,只報 Fizz (忽略規則 一、二、三、4)
    • 若是不是特殊數字的倍數,而且不包含第一個特殊數字,就報對應的序號。
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();
    }
}
複製代碼

此時代碼的「壞味道」逐漸展現出來,須要引入重構階段來消除重複設計,讓小步快跑的節奏更加踏實。

閱讀系列文章:

源碼

github.com/lynings/tdd…


歡迎關注個人微信訂閱號,我將會持續輸出更多技術文章,但願咱們能夠互相學習。

相關文章
相關標籤/搜索