文章目录

  • 使用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项目的过程如下:

  1. File / New Project / Maven;
  2. 勾选"Create from archetype",选择org.apache.maven.archetypes:maven-archetype-quickstart
  3. 输入Name为项目名称,选择Location为项目路径,展开Artifact Coordinates,输入GroupId为包路径,ArtifactId默认为项目名称;
  4. 确认信息无误后,开始创建项目;
  5. 点击“Open Windows”打开项目;
  6. 在弹出框中选择“Enable Auto-Import”;
  7. 编辑项目pom.xml,将maven.compiler.sourcemaven.compiler.target改为1.8
  8. 右键选择项目,Maven / Reimport;
  9. 删除自动生成的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;@Beforepublic void setUp() {game = new Game();}@Testpublic void testGutterGame() {for (int i = 0; i < 20; i ++) {game.roll(0);}assertEquals(0, game.score());}@Testpublic 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;@Beforepublic void setUp() {game = new Game();}@Testpublic 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);}}@Testpublic 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); // sparegame.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 rollscore += 10 + rolls[rollIndex + 2];// move to next framerollIndex += 2;} else {// other: sum of balls in framescore += rolls[rollIndex] + rolls[rollIndex + 1];// move to next framerollIndex += 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 framerollIndex += 2;} else {// other: sum of balls in framescore += rolls[rollIndex] + rolls[rollIndex + 1];// move to next framerollIndex += 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); // strikegame.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 framerollIndex += 2;} else {// other: sum of balls in framescore += rolls[rollIndex] + rolls[rollIndex + 1];// move to next framerollIndex += 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

使用Intellij来实践测试驱动开发 TDD Kata相关推荐

  1. 「敏捷架构」核心实践:测试驱动开发(TDD)简介

    测试驱动开发(TDD) 是一种渐进的开发方法,它结合了测试优先的开发,即在编写足够的产品代码以完成测试和重构之前编写测试.TDD的主要目标是什么?一个观点是TDD的目标是规范而不是验证(Martin, ...

  2. 测试驱动开发(TDD)的实践

    测试驱动开发(TDD)的实践 本文作者: Mr.J 本文链接: https://jiangtj.com/articles/almond/test-driven%20development/ 测试驱动开 ...

  3. Python测试驱动开发(TDD)

    Python测试驱动开发(TDD) 前言:TDD是一种敏捷开发模式,而不是测试方法. 测试很难 --- 难在坚持,一直做下去. 现在花时间编写的测试不会立即显出功效,要等到很久以后才有作用 --- 或 ...

  4. 测试驱动开发(TDD)实战小例子(JAVA版)

    我们知道,测试驱动开发(TDD)的基本思想就是在开发功能代码之前,先编写测试代码.也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用 ...

  5. 测试驱动开发-TDD

    测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法.它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能 ...

  6. java测试驱动开发_java测试驱动开发(TDD)之《遥控军舰》

    永久更新地址:https://my.oschina.net/bysu/blog/1647738 写在前面:若有侵权,请发邮件by.su@qq.com告知. 本文主要是学习<Java测试驱动开发& ...

  7. C++ 测试驱动开发 TDD(一)

    文章目录 TDD 介绍 Soundex 算法示例介绍 增加Soundex 算法测例1 增加Soundex 算法测例2 Soundex 算法测例1 .2重构 后记 最近阅读了<C++程序设计实践与 ...

  8. java测试驱动开发(TDD)之《井字游戏》

    为什么80%的码农都做不了架构师?>>>    永久更新地址:https://my.oschina.net/bysu/blog/1632393 写在前面:若有侵权,请发邮件by.su ...

  9. 测试驱动开发-TDD(1)

    测试:作为动词,它是评估的意思:作为名词,它是导致最终是接受还是不接受的过程. 测试是相互独立的. 测试列表,就跟你生活中记录你的工作计划一样. 测试优先:你应该在什么时候编写测试呢?在你编写要被测试 ...

  10. tdd测试驱动开发课程介绍_测试驱动开发的实用介绍

    tdd测试驱动开发课程介绍 by Luca Piccinelli 通过卢卡·皮奇内利 测试驱动开发很难! 这是不为人知的事实. (Test Driven Development is hard! Th ...

最新文章

  1. 2022-2028年中国环卫行业产业链深度调研及投资前景预测报告
  2. DL645规约学习笔记-一帧数据解释
  3. UVa 164 - String Computer
  4. java.sql.SQLException: Column ‘class‘ not found.异常没有找到列
  5. 58回应“简历数据泄露”:将展开追查并加固安全系统
  6. Nacos 1.3.0 发布,一个修炼内功的版本:全新内核构建!
  7. java实现对HDFS增删改查(CRUD)等操作
  8. 机器学习实践:onnx模型转为Tensorflow2的pb模型2020
  9. MOCTF-Web-机器蛇
  10. html5填空题阅卷,“过五关”般严格,高考阅卷老师来自哪里?研究生参与阅卷?
  11. openstack 云_使用OpenStack打造云事业
  12. LeetCode -- 推断链表中是否有环
  13. 软件如何上传虚拟服务器,本地软件传到虚拟机上
  14. android webview 设置下载文件,如何使用Android webview下载文本文件
  15. App 快捷方式——创建快捷方式
  16. 面试时如何做自我介绍?聪明的应聘者只聚焦1点
  17. windows10和linux流畅,《古墓丽影10:崛起》Linux平台与Windows平台流畅度对比
  18. 自己定义微信图文模板注意事项整理
  19. python用re模块实现数学公式计算
  20. 工业机械设备设计与艺术设计

热门文章

  1. SciPy教程 - 稀疏矩阵库scipy.sparse
  2. GIT远程分支误删恢复
  3. Matlab 校验方法
  4. 物理综合 | congestion 报告解读
  5. 美国亚马逊编辑推荐的一生必读书单100本
  6. 三极管的工作原理(详细、通俗易懂、图文并茂)
  7. android 高德地图全套,android快速集成高德地图
  8. 阿里云cdn设置不缓存
  9. npm i 和 npm i -S有什么区别吗?
  10. 群晖nas不能修改php.ini,群晖nas 修改nginx配置的问题