文章目錄
- 使用Intellij來實踐測試驅動開發 TDD Kata
- 前言
- 創建Java Maven項目
- TheBowlingGame Kata
- The Requirements
- Step1: 創建項目
- Step2: 新建測試類
- Step3: 編寫第1個測試方法
- Step4: 運行測試
- Step5: 修復編譯錯誤
- Step6: 再次運行測試
- Step7: 繼續修改測試方法
- Step8: 修復編譯錯誤
- Step9: 編寫第2個測試方法
- Step10: 修復測試失敗
- Step11: 重構測試類
- Step12: 繼續重構測試類
- Step12: 編寫第3個測試方法
- Step13: 修復測試失敗
- Step14: 繼續修復測試失敗
- Step15: 重構實現類
- Step16: 重構測試類
- Step17: 編寫第4個測試方法
- Step18: 修復測試失敗
- Step19: 重構實現類
- Step20: 繼續重構實現類
- Step21: 繼續重構實現類
- Step22: 繼續重構實現類
- Step23: 重構測試類
- Step24: 編寫第5個測試方法
- Step25: 重構實現類
- TDD Kata小結
- Intellij常用快捷鍵
- 如何提升打字速度
- 參考文檔
使用Intellij來實踐測試驅動開發 TDD Kata
前言
本文描述瞭如何使用Intellij來實踐測試驅動開發(TDD Kata)。
編程環境:
- Intellij IDEA 2019.3 (Mac版)
- Java 8
- Maven 3.6(配置使用阿里雲Maven鏡像)
- JUnit 4
創建Java Maven項目
IDEA中創建一個標準的含有JUnit的Java Maven項目的過程如下:
- File / New Project / Maven;
- 勾選"Create from archetype",選擇
org.apache.maven.archetypes:maven-archetype-quickstart
- 輸入Name爲項目名稱,選擇Location爲項目路徑,展開Artifact Coordinates,輸入GroupId爲包路徑,ArtifactId默認爲項目名稱;
- 確認信息無誤後,開始創建項目;
- 點擊“Open Windows”打開項目;
- 在彈出框中選擇“Enable Auto-Import”;
- 編輯項目pom.xml,將
maven.compiler.source
和maven.compiler.target
改爲1.8
; - 右鍵選擇項目,Maven / Reimport;
- 刪除自動生成的App和AppTest類。
也可以自定義Maven archetype,通用運行Maven命令一鍵生成項目。
TheBowlingGame Kata
以Bob大叔的Bowling Game Kata爲例,講解如何通過IDEA來練習TDD Kata。
保齡球比賽計分規則:
-
保齡球的計分不難,每一局(Game)總共有十格(Frame),每一格里面有兩次投球(Roll)。
-
共有十支球瓶,要儘量在兩次投球之內把球瓶(Pin)全部擊倒,如果第一球就把全部的球瓶都擊倒了,也就是“STRIKE”,畫面出現“X”,就算完成一格了,所得分數就是10分再加下兩球的倒瓶數。
-
但是如果第一球沒有全倒時,就要再打一球,如果剩下的球瓶全都擊倒,也就是“SPARE”,畫面出現“/”,也算完成一格,所得分數爲10分再加下一格第一球的倒瓶數。
-
但是如果第二球也沒有把球瓶全部擊倒的話,那分數就是第一球加第二球倒的瓶數,再接着打下一格。
-
依此類推直到第十格,但是第十格有三球,第十格時如果第一球或第二球將球瓶全部擊倒時,可再加打第三球。
參見:
在練習時只使用英文輸入法,避免頻繁切換輸入法,和避免在中文輸入法時IDEA快捷鍵不生效。
The Requirements
Write a class named Game
that has two methods:
roll(pins : int)
is called each time the player rolls a ball.The argument is the number of pins knocked down.score()
: int is called only at the very end of the game. It returns the total score for that game.
Step1: 創建項目
參見上面的“創建Java Maven項目”章節來創建項目:
- 項目名稱:
bowling-game-kata
- GroupId:
cn.xdevops.kata
- ArtifactId:
bowling-game-kata
Step2: 新建測試類
在/src/test/java/cn.xdevops.kata
目錄下創建測試類GameTest。
選擇/src/test/java/cn.xdevops.kata
目錄:
# 新建
Ctrl + N
# 默認選擇新建Java Calss
Enter
# 輸入類名
GameTest
# 確認新建
Enter
# 光標跳到下一行
Shift + Enter
Step3: 編寫第1個測試方法
編寫一個最差的比賽結果(每次投球都沒有擊倒瓶子)的測試方法:
@Test
public void testGutterGame() {
Game game = new Game();
}
因爲Game類還沒有創建,此時有編譯錯誤,先忽略。
Step4: 運行測試
將光標移動到GameTest類名一行:
# 運行測試
Shift + F10
因爲Game類還沒有創建,此時有編譯錯誤,所以測試失敗(紅)。
Step5: 修復編譯錯誤
將光標移動到Game類名上:
# 自動修復錯誤
Alter + Enter
# 默認選擇Create class
Enter
# 確認生成類
Enter
Step6: 再次運行測試
# 運行測試
Shift + F10
此時,測試通過(綠)。
Step7: 繼續修改測試方法
切換回測試類GameTest:
# 切換類
Command + E
# 切換爲上一個打開的類
Enter
在測試方法中增加邏輯:
@Test
public void testGutterGame() {
Game game = new Game();
for (int i = 0; i < 20; i ++) {
game.roll(0);
}
assertEquals(0, game.score());
}
Step8: 修復編譯錯誤
因爲有很明顯的編譯錯誤,所以我們不再運行測試類,而是先修復編譯錯誤。
用Alt
+ Enter
來修復編譯錯誤:
- 在
Game
類中創建roll()
方法,修改參數名爲pins
; - 引入
assertEquals
; - 在
Game
類中創建score()
方法,返回值爲0
。
代碼示例:
public class Game {
public void roll(int pins) {
}
public int score() {
return 0;
}
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step9: 編寫第2個測試方法
增加一個簡單的測試方法,假設每次投球都擊倒1個瓶子。
有了前面的經驗,我們可以很快地寫出這個測試方法。
代碼示例:
@Test
public void testAllOnes() {
Game game = new Game();
for (int i = 0; i < 20; i ++) {
game.roll(1);
}
assertEquals(20, game.score());
}
按下Shift
+ F10
運行測試,因爲我們還沒有實現該功能,測試失敗(紅)。
Step10: 修復測試失敗
很容易想到,只要將每次投球擊倒的瓶子數量累加來作爲最後的分數就可以了。
代碼示例:
public class Game {
private int score = 0;
public void roll(int pins) {
score += pins;
}
public int score() {
return score;
}
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step11: 重構測試類
因爲測試類中存在了重複代碼,因此在繼續編寫新的測試方法前,需要先重構測試類。
將每個測試方法中的創建Game實例的語句抽取出來,寫成JUnit的setUp()方法。
public class GameTest {
private Game game;
@Before
public void setUp() {
game = new Game();
}
@Test
public void testGutterGame() {
for (int i = 0; i < 20; i ++) {
game.roll(0);
}
assertEquals(0, game.score());
}
@Test
public void testAllOnes() {
for (int i = 0; i < 20; i ++) {
game.roll(1);
}
assertEquals(20, game.score());
}
}
按下Shift
+ F10
運行測試,確保在重構後,測試仍然通過(綠)。
Step12: 繼續重構測試類
將測試類中的投20個球,每個球都擊倒一樣瓶子的方法抽取成一個方法:
# 選擇重複代碼
# 提取方法
Alt + Comand + M
# 輸入方法
rollMany
# Refactor
Enter
# 選擇使用原方法簽名或接受推薦的方法簽名
Enter
# 選擇是否替換其他重複代碼
Enter
修改rollMany()方法,支持傳入指定的球數,並修改方法參數(Shift
+ F6
):
private void rollMany(int rolls, int pins) {
for (int i = 0; i < rolls; i++) {
game.roll(pins);
}
}
修改調用rollMany()方法的語句,傳入指定的球數爲20。
完整代碼:
public class GameTest {
private Game game;
@Before
public void setUp() {
game = new Game();
}
@Test
public void testGutterGame() {
rollMany(20, 0);
assertEquals(0, game.score());
}
private void rollMany(int rolls, int pins) {
for (int i = 0; i < rolls; i++) {
game.roll(pins);
}
}
@Test
public void testAllOnes() {
rollMany(20, 1);
assertEquals(20, game.score());
}
}
按下Shift
+ F10
運行測試,確保在重構後,測試仍然通過(綠)。
Step12: 編寫第3個測試方法
增加一個測試方法,來測試第一個Frame爲Spare的情況。
@Test
public void testOneSpare() {
game.roll(4);
game.roll(6); // spare
game.roll(3);
rollMany(17, 0);
assertEquals(16, game.score());
}
在這個例子中,第一個Frame爲Spare,所以第一個Frame的得分要加上該Frame的下一個球擊倒的球數(Spare bonus)。爲簡單起見,假設後面17個球都沒有擊倒瓶子。
按下Shift
+ F10
運行測試,測試失敗(紅),因爲Game類還沒有考慮加上Spare bonus的情況。
Step13: 修復測試失敗
在保齡球比賽中:
- 如果一個Frame是Spare,該Frame的得分要加上下一個球擊倒的瓶子數(Spare bonus);
- 如果一個Frame是Strike,該Frame的得分要加上下兩個球擊倒的瓶子數(Strike bonus)。
所以,我們首先要記住每次投球所擊倒的瓶子數,因爲最多投球21次(每Frame最多投2球,最後一個Frame最多投3個球),所以用一個長度爲21的int數組來記。
再用一個變量來記住當前是第幾個球。
代碼示例:
private int[] rolls = new int[21];
private int currentRoll = 0;
在每次投球時記住該次投球擊倒的瓶子數,並記錄當前是第幾個球:
public void roll(int pins) {
rolls[currentRoll] = pins;
currentRoll ++;
}
在計算比賽得分時,累加全部球擊倒的瓶子數加上Spare bonus和Strike bonus。
先累加全部球擊倒的瓶子數:
public class Game {
private int[] rolls = new int[21];
private int currentRoll = 0;
public void roll(int pins) {
rolls[currentRoll] = pins;
currentRoll ++;
}
public int score() {
int score = 0;
for (int i = 0; i < rolls.length;i ++) {
score += rolls[i];
}
return score;
}
}
按下Shift
+ F10
運行測試,發現其他不需要計算bonus的測試方法測試通過,但是需要計算Spare bonus的方法仍然測試不通過。
Step14: 繼續修復測試失敗
繼續實現計算Spare bonus的邏輯:
- Spare時,該Frame的得分爲10分加上下一次投球擊倒的瓶子數;
- 其他情況時,該Frame的得分爲兩次投球擊倒的瓶子總數。(先不考慮Strike的情況)
public int score() {
int score = 0;
int rollIndex = 0;
for (int frame = 0; frame < 10; frame ++) {
if (rolls[rollIndex] + rolls[rollIndex + 1] == 10) {
// spare: sum of balls in frame is 10
// spare bonus: balls of next roll
score += 10 + rolls[rollIndex + 2];
// move to next frame
rollIndex += 2;
} else {
// other: sum of balls in frame
score += rolls[rollIndex] + rolls[rollIndex + 1];
// move to next frame
rollIndex += 2;
}
}
return score;
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step15: 重構實現類
重構Game類的score()方法,去除註釋,讓代碼能夠“自描述”。
提取出一個專門的方法用來判斷是否爲Spare:
private boolean isSpare(int rollIndex) {
return rolls[rollIndex] + rolls[rollIndex + 1] == 10;
}
按下Shift
+ F10
運行測試,測試通過(綠)。
去掉Spare部分的註釋:
public int score() {
int score = 0;
int rollIndex = 0;
for (int frame = 0; frame < 10; frame++) {
if (isSpare(rollIndex)) {
score += 10 + rolls[rollIndex + 2];
// move to next frame
rollIndex += 2;
} else {
// other: sum of balls in frame
score += rolls[rollIndex] + rolls[rollIndex + 1];
// move to next frame
rollIndex += 2;
}
}
return score;
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step16: 重構測試類
重構GameTest類的testOneSpare()方法。
提取出一個專門的rollSpare()方法。
private void rollSpare() {
game.roll(4);
game.roll(6);
}
按下Shift
+ F10
運行測試,測試通過(綠)。
重構後的testOneSpare()方法:
@Test
public void testOneSpare() {
rollSpare();
game.roll(3);
rollMany(17, 0);
assertEquals(16, game.score());
}
Step17: 編寫第4個測試方法
增加一個測試方法,來測試第一個Frame爲Strike的情況。
@Test
public void testOneStrike() {
game.roll(10); // strike
game.roll(3);
game.roll(4);
rollMany(16, 0);
assertEquals(24, game.score());
}
在這個例子中,第一個Frame爲Strike,所以第一個Frame的得分要加上該Frame的下兩個球擊倒的球數(Strike bonus)。爲簡單起見,假設後面16個球都沒有擊倒瓶子。
按下Shift
+ F10
運行測試,測試失敗(紅),因爲Game類還沒有考慮加上Strike bonus的情況。
Step18: 修復測試失敗
增加計算Strik bonus的情況。
public int score() {
int score = 0;
int rollIndex = 0;
for (int frame = 0; frame < 10; frame++) {
if (rolls[rollIndex] == 10) {
score += 10 + rolls[rollIndex + 1] + rolls[rollIndex + 2];
rollIndex += 1;
} else if (isSpare(rollIndex)) {
score += 10 + rolls[rollIndex + 2];
// move to next frame
rollIndex += 2;
} else {
// other: sum of balls in frame
score += rolls[rollIndex] + rolls[rollIndex + 1];
// move to next frame
rollIndex += 2;
}
}
return score;
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step19: 重構實現類
提取出一個專門的方法用來判斷是否爲Strike:
private boolean isStrike(int rollIndex) {
return rolls[rollIndex] == 10;
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step20: 繼續重構實現類
提取出一個專門的方法用來計算Strike bonus。
private int strikeBonus(int rollIndex) {
return rolls[rollIndex + 1] + rolls[rollIndex + 2];
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step21: 繼續重構實現類
提取出一個專門的方法用來計算Spare bonus。
private int spareBonus(int rollIndex) {
return rolls[rollIndex + 2];
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step22: 繼續重構實現類
提取出一個專門的方法用來計算普通的Frame的得分。
private int sumOfBallsInFrame(int rollIndex) {
return rolls[rollIndex] + rolls[rollIndex + 1];
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step23: 重構測試類
重構GameTest類的testOneStrike()方法。
提取出一個專門的rollStrike()方法。
private void rollStrike() {
game.roll(10);
}
按下Shift
+ F10
運行測試,測試通過(綠)。
重構後的testOneStrike()方法:
@Test
public void testOneStrike() {
rollStrike();
game.roll(3);
game.roll(4);
rollMany(16, 0);
assertEquals(24, game.score());
}
Step24: 編寫第5個測試方法
增加一個測試方法,來測試最完美的情況,也就是連續12個球都把10個瓶子擊倒了,該局比賽得分爲300分。
@Test
public void testPerfectGame() {
rollMany(12, 10);
assertEquals(300, game.score());
}
按下Shift
+ F10
運行測試,測試通過(綠)。
Step25: 重構實現類
將Game類的一些hardcode改爲爲常量。
直接將hardcode改爲常量,再用Alt
+ Enter
自動生成常量。
按下Shift
+ F10
運行測試,測試通過(綠)。
再去除多餘的註釋。
按下Shift
+ F10
運行測試,測試通過(綠)。
重構後的代碼:
package cn.xdevops.kata;
public class Game {
private static final int MAX_ROLL_NUM = 21;
private static final int MAX_FRAME_NUM = 10;
private static final int MAX_PIN_NUM = 10;
private int[] rolls = new int[MAX_ROLL_NUM];
private int currentRoll = 0;
public void roll(int pins) {
rolls[currentRoll] = pins;
currentRoll++;
}
public int score() {
int score = 0;
int rollIndex = 0;
for (int frame = 0; frame < MAX_FRAME_NUM; frame++) {
if (isStrike(rollIndex)) {
score += MAX_PIN_NUM + strikeBonus(rollIndex);
rollIndex += 1;
} else if (isSpare(rollIndex)) {
score += MAX_PIN_NUM + spareBonus(rollIndex);
rollIndex += 2;
} else {
score += sumOfBallsInFrame(rollIndex);
rollIndex += 2;
}
}
return score;
}
private int sumOfBallsInFrame(int rollIndex) {
return rolls[rollIndex] + rolls[rollIndex + 1];
}
private int spareBonus(int rollIndex) {
return rolls[rollIndex + 2];
}
private int strikeBonus(int rollIndex) {
return rolls[rollIndex + 1] + rolls[rollIndex + 2];
}
private boolean isStrike(int rollIndex) {
return rolls[rollIndex] == MAX_PIN_NUM;
}
private boolean isSpare(int rollIndex) {
return rolls[rollIndex] + rolls[rollIndex + 1] == MAX_PIN_NUM;
}
}
至此,一個功能正確,且代碼整潔的BowlingGame Kata就完成了。
TDD Kata小結
TDD心法小結;
-
紅-綠-重構,不斷迭代;
-
小步快跑(步子大了容易走火入魔)
-
增加新功能時不重構,重構時不增加新功能
剛開始練習TDD Kata時步子要小,可以慢一點。等熟練後,就要刻意練習加快速度,可以用秒錶來統計Kata用時。
一般來說一個Kata的用時,不應該超過1個番茄鍾(25分鐘)。可以通過以下方法提升速度:
- 只用英文輸入法,用英文寫註釋;
- 熟悉Intellij快捷鍵操作,儘量少用鼠標;
- 熟練掌握編程語言的語法和常用API(編程時不去搜索語法);
- 平時有意練習打字速度;
- 多多練習不同的Kata,用不同的編程語言來實現。
Intellij常用快捷鍵
編程中常用的快捷鍵:
名稱 | 快捷鍵(Mac) |
---|---|
新建 | Ctrl + N |
切換 | Command + E |
自動修復 | Alt + Enter |
運行 | Shift + F10 |
重命名 | Shift + F6 |
提取方法 | Alt + Command + M |
提取變量 | Alt + Command + V |
光標跳到下一行 | Shift + Enter |
光標跳到行首 | Fn + Left |
光標跳到行尾 | Fn + Right |
註釋/取消註釋 | Command + / |
格式化 | Alt + Command + L |
按下CTRL
+ SHIFT
+ A
快速查找相應Action (菜單)。
參見
如何提升打字速度
可以在https://typing.io/lessons上測試和練習多種編程語言的打字速度。