使用Intellij来实践测试驱动开发 TDD Kata
文章目录
- 使用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命令一键生成项目:
mvn -B archetype:generate \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=RELEASE \
-DgroupId=cn.xdevops.kata \
-DartifactId=bowling-game-kata
TheBowlingGame Kata
以Bob大叔的Bowling Game Kata为例,讲解如何通过IDEA来练习TDD Kata。
- TheBowlingGameKata
保龄球比赛计分规则:
- 保龄球的计分不难,每一局(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 (菜单)。
参见
- Intellj快捷键官方文档
- IDEA Windows快捷键
- IDEA MacOS快捷键
如何提升打字速度
可以在https://typing.io/lessons上测试和练习多种编程语言的打字速度。
参考文档
- https://www.jetbrains.com/help/idea/tdd-with-intellij-idea.html
- https://www.jetbrains.com/help/idea/refactoring-source-code.html
- 软件匠艺 Software Craftsmanship
还没有评论,来说两句吧...