使用Intellij来实践测试驱动开发 TDD Kata

墨蓝 2023-02-22 08:10 33阅读 0赞

文章目录

  • 使用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命令一键生成项目:

  1. mvn -B archetype:generate \
  2. -DarchetypeGroupId=org.apache.maven.archetypes \
  3. -DarchetypeArtifactId=maven-archetype-quickstart \
  4. -DarchetypeVersion=RELEASE \
  5. -DgroupId=cn.xdevops.kata \
  6. -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目录:

  1. # 新建
  2. Ctrl + N
  3. # 默认选择新建Java Calss
  4. Enter
  5. # 输入类名
  6. GameTest
  7. # 确认新建
  8. Enter
  9. # 光标跳到下一行
  10. Shift + Enter

Step3: 编写第1个测试方法

编写一个最差的比赛结果(每次投球都没有击倒瓶子)的测试方法:

  1. @Test
  2. public void testGutterGame() {
  3. Game game = new Game();
  4. }

因为Game类还没有创建,此时有编译错误,先忽略。

Step4: 运行测试

将光标移动到GameTest类名一行:

  1. # 运行测试
  2. Shift + F10

因为Game类还没有创建,此时有编译错误,所以测试失败(红)。

Step5: 修复编译错误

将光标移动到Game类名上:

  1. # 自动修复错误
  2. Alter + Enter
  3. # 默认选择Create class
  4. Enter
  5. # 确认生成类
  6. Enter

Step6: 再次运行测试

  1. # 运行测试
  2. Shift + F10

此时,测试通过(绿)。

Step7: 继续修改测试方法

切换回测试类GameTest:

  1. # 切换类
  2. Command + E
  3. # 切换为上一个打开的类
  4. Enter

在测试方法中增加逻辑:

  1. @Test
  2. public void testGutterGame() {
  3. Game game = new Game();
  4. for (int i = 0; i < 20; i ++) {
  5. game.roll(0);
  6. }
  7. assertEquals(0, game.score());
  8. }

Step8: 修复编译错误

因为有很明显的编译错误,所以我们不再运行测试类,而是先修复编译错误。

Alt + Enter来修复编译错误:

  • Game类中创建roll()方法,修改参数名为pins
  • 引入assertEquals
  • Game类中创建score()方法,返回值为0

代码示例:

  1. public class Game {
  2. public void roll(int pins) {
  3. }
  4. public int score() {
  5. return 0;
  6. }
  7. }

按下Shift + F10运行测试,测试通过(绿)。

Step9: 编写第2个测试方法

增加一个简单的测试方法,假设每次投球都击倒1个瓶子。

有了前面的经验,我们可以很快地写出这个测试方法。

代码示例:

  1. @Test
  2. public void testAllOnes() {
  3. Game game = new Game();
  4. for (int i = 0; i < 20; i ++) {
  5. game.roll(1);
  6. }
  7. assertEquals(20, game.score());
  8. }

按下Shift + F10运行测试,因为我们还没有实现该功能,测试失败(红)。

Step10: 修复测试失败

很容易想到,只要将每次投球击倒的瓶子数量累加来作为最后的分数就可以了。

代码示例:

  1. public class Game {
  2. private int score = 0;
  3. public void roll(int pins) {
  4. score += pins;
  5. }
  6. public int score() {
  7. return score;
  8. }
  9. }

按下Shift + F10运行测试,测试通过(绿)。

Step11: 重构测试类

因为测试类中存在了重复代码,因此在继续编写新的测试方法前,需要先重构测试类。

将每个测试方法中的创建Game实例的语句抽取出来,写成JUnit的setUp()方法。

  1. public class GameTest {
  2. private Game game;
  3. @Before
  4. public void setUp() {
  5. game = new Game();
  6. }
  7. @Test
  8. public void testGutterGame() {
  9. for (int i = 0; i < 20; i ++) {
  10. game.roll(0);
  11. }
  12. assertEquals(0, game.score());
  13. }
  14. @Test
  15. public void testAllOnes() {
  16. for (int i = 0; i < 20; i ++) {
  17. game.roll(1);
  18. }
  19. assertEquals(20, game.score());
  20. }
  21. }

按下Shift + F10运行测试,确保在重构后,测试仍然通过(绿)。

Step12: 继续重构测试类

将测试类中的投20个球,每个球都击倒一样瓶子的方法抽取成一个方法:

  1. # 选择重复代码
  2. # 提取方法
  3. Alt + Comand + M
  4. # 输入方法
  5. rollMany
  6. # Refactor
  7. Enter
  8. # 选择使用原方法签名或接受推荐的方法签名
  9. Enter
  10. # 选择是否替换其他重复代码
  11. Enter

修改rollMany()方法,支持传入指定的球数,并修改方法参数(Shift + F6):

  1. private void rollMany(int rolls, int pins) {
  2. for (int i = 0; i < rolls; i++) {
  3. game.roll(pins);
  4. }
  5. }

修改调用rollMany()方法的语句,传入指定的球数为20。

完整代码:

  1. public class GameTest {
  2. private Game game;
  3. @Before
  4. public void setUp() {
  5. game = new Game();
  6. }
  7. @Test
  8. public void testGutterGame() {
  9. rollMany(20, 0);
  10. assertEquals(0, game.score());
  11. }
  12. private void rollMany(int rolls, int pins) {
  13. for (int i = 0; i < rolls; i++) {
  14. game.roll(pins);
  15. }
  16. }
  17. @Test
  18. public void testAllOnes() {
  19. rollMany(20, 1);
  20. assertEquals(20, game.score());
  21. }
  22. }

按下Shift + F10运行测试,确保在重构后,测试仍然通过(绿)。

Step12: 编写第3个测试方法

增加一个测试方法,来测试第一个Frame为Spare的情况。

  1. @Test
  2. public void testOneSpare() {
  3. game.roll(4);
  4. game.roll(6); // spare
  5. game.roll(3);
  6. rollMany(17, 0);
  7. assertEquals(16, game.score());
  8. }

在这个例子中,第一个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数组来记。

再用一个变量来记住当前是第几个球。

代码示例:

  1. private int[] rolls = new int[21];
  2. private int currentRoll = 0;

在每次投球时记住该次投球击倒的瓶子数,并记录当前是第几个球:

  1. public void roll(int pins) {
  2. rolls[currentRoll] = pins;
  3. currentRoll ++;
  4. }

在计算比赛得分时,累加全部球击倒的瓶子数加上Spare bonus和Strike bonus。

先累加全部球击倒的瓶子数:

  1. public class Game {
  2. private int[] rolls = new int[21];
  3. private int currentRoll = 0;
  4. public void roll(int pins) {
  5. rolls[currentRoll] = pins;
  6. currentRoll ++;
  7. }
  8. public int score() {
  9. int score = 0;
  10. for (int i = 0; i < rolls.length;i ++) {
  11. score += rolls[i];
  12. }
  13. return score;
  14. }
  15. }

按下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 ++) {

    1. if (rolls[rollIndex] + rolls[rollIndex + 1] == 10) {
    2. // spare: sum of balls in frame is 10
    3. // spare bonus: balls of next roll
    4. score += 10 + rolls[rollIndex + 2];
    5. // move to next frame
    6. rollIndex += 2;
    7. } else {
    8. // other: sum of balls in frame
    9. score += rolls[rollIndex] + rolls[rollIndex + 1];
    10. // move to next frame
    11. rollIndex += 2;
    12. }

    }
    return score;
    }

按下Shift + F10运行测试,测试通过(绿)。

Step15: 重构实现类

重构Game类的score()方法,去除注释,让代码能够“自描述”。

提取出一个专门的方法用来判断是否为Spare:

  1. private boolean isSpare(int rollIndex) {
  2. return rolls[rollIndex] + rolls[rollIndex + 1] == 10;
  3. }

按下Shift + F10运行测试,测试通过(绿)。

去掉Spare部分的注释:

  1. public int score() {
  2. int score = 0;
  3. int rollIndex = 0;
  4. for (int frame = 0; frame < 10; frame++) {
  5. if (isSpare(rollIndex)) {
  6. score += 10 + rolls[rollIndex + 2];
  7. // move to next frame
  8. rollIndex += 2;
  9. } else {
  10. // other: sum of balls in frame
  11. score += rolls[rollIndex] + rolls[rollIndex + 1];
  12. // move to next frame
  13. rollIndex += 2;
  14. }
  15. }
  16. return score;
  17. }

按下Shift + F10运行测试,测试通过(绿)。

Step16: 重构测试类

重构GameTest类的testOneSpare()方法。

提取出一个专门的rollSpare()方法。

  1. private void rollSpare() {
  2. game.roll(4);
  3. game.roll(6);
  4. }

按下Shift + F10运行测试,测试通过(绿)。

重构后的testOneSpare()方法:

  1. @Test
  2. public void testOneSpare() {
  3. rollSpare();
  4. game.roll(3);
  5. rollMany(17, 0);
  6. assertEquals(16, game.score());
  7. }

Step17: 编写第4个测试方法

增加一个测试方法,来测试第一个Frame为Strike的情况。

  1. @Test
  2. public void testOneStrike() {
  3. game.roll(10); // strike
  4. game.roll(3);
  5. game.roll(4);
  6. rollMany(16, 0);
  7. assertEquals(24, game.score());
  8. }

在这个例子中,第一个Frame为Strike,所以第一个Frame的得分要加上该Frame的下两个球击倒的球数(Strike bonus)。为简单起见,假设后面16个球都没有击倒瓶子。

按下Shift + F10运行测试,测试失败(红),因为Game类还没有考虑加上Strike bonus的情况。

Step18: 修复测试失败

增加计算Strik bonus的情况。

  1. public int score() {
  2. int score = 0;
  3. int rollIndex = 0;
  4. for (int frame = 0; frame < 10; frame++) {
  5. if (rolls[rollIndex] == 10) {
  6. score += 10 + rolls[rollIndex + 1] + rolls[rollIndex + 2];
  7. rollIndex += 1;
  8. } else if (isSpare(rollIndex)) {
  9. score += 10 + rolls[rollIndex + 2];
  10. // move to next frame
  11. rollIndex += 2;
  12. } else {
  13. // other: sum of balls in frame
  14. score += rolls[rollIndex] + rolls[rollIndex + 1];
  15. // move to next frame
  16. rollIndex += 2;
  17. }
  18. }
  19. return score;
  20. }

按下Shift + F10运行测试,测试通过(绿)。

Step19: 重构实现类

提取出一个专门的方法用来判断是否为Strike:

  1. private boolean isStrike(int rollIndex) {
  2. return rolls[rollIndex] == 10;
  3. }

按下Shift + F10运行测试,测试通过(绿)。

Step20: 继续重构实现类

提取出一个专门的方法用来计算Strike bonus。

  1. private int strikeBonus(int rollIndex) {
  2. return rolls[rollIndex + 1] + rolls[rollIndex + 2];
  3. }

按下Shift + F10运行测试,测试通过(绿)。

Step21: 继续重构实现类

提取出一个专门的方法用来计算Spare bonus。

  1. private int spareBonus(int rollIndex) {
  2. return rolls[rollIndex + 2];
  3. }

按下Shift + F10运行测试,测试通过(绿)。

Step22: 继续重构实现类

提取出一个专门的方法用来计算普通的Frame的得分。

  1. private int sumOfBallsInFrame(int rollIndex) {
  2. return rolls[rollIndex] + rolls[rollIndex + 1];
  3. }

按下Shift + F10运行测试,测试通过(绿)。

Step23: 重构测试类

重构GameTest类的testOneStrike()方法。

提取出一个专门的rollStrike()方法。

  1. private void rollStrike() {
  2. game.roll(10);
  3. }

按下Shift + F10运行测试,测试通过(绿)。

重构后的testOneStrike()方法:

  1. @Test
  2. public void testOneStrike() {
  3. rollStrike();
  4. game.roll(3);
  5. game.roll(4);
  6. rollMany(16, 0);
  7. assertEquals(24, game.score());
  8. }

Step24: 编写第5个测试方法

增加一个测试方法,来测试最完美的情况,也就是连续12个球都把10个瓶子击倒了,该局比赛得分为300分。

  1. @Test
  2. public void testPerfectGame() {
  3. rollMany(12, 10);
  4. assertEquals(300, game.score());
  5. }

按下Shift + F10运行测试,测试通过(绿)。

Step25: 重构实现类

将Game类的一些hardcode改为为常量。

直接将hardcode改为常量,再用Alt + Enter自动生成常量。

按下Shift + F10运行测试,测试通过(绿)。

再去除多余的注释。

按下Shift + F10运行测试,测试通过(绿)。

重构后的代码:

  1. package cn.xdevops.kata;
  2. public class Game {
  3. private static final int MAX_ROLL_NUM = 21;
  4. private static final int MAX_FRAME_NUM = 10;
  5. private static final int MAX_PIN_NUM = 10;
  6. private int[] rolls = new int[MAX_ROLL_NUM];
  7. private int currentRoll = 0;
  8. public void roll(int pins) {
  9. rolls[currentRoll] = pins;
  10. currentRoll++;
  11. }
  12. public int score() {
  13. int score = 0;
  14. int rollIndex = 0;
  15. for (int frame = 0; frame < MAX_FRAME_NUM; frame++) {
  16. if (isStrike(rollIndex)) {
  17. score += MAX_PIN_NUM + strikeBonus(rollIndex);
  18. rollIndex += 1;
  19. } else if (isSpare(rollIndex)) {
  20. score += MAX_PIN_NUM + spareBonus(rollIndex);
  21. rollIndex += 2;
  22. } else {
  23. score += sumOfBallsInFrame(rollIndex);
  24. rollIndex += 2;
  25. }
  26. }
  27. return score;
  28. }
  29. private int sumOfBallsInFrame(int rollIndex) {
  30. return rolls[rollIndex] + rolls[rollIndex + 1];
  31. }
  32. private int spareBonus(int rollIndex) {
  33. return rolls[rollIndex + 2];
  34. }
  35. private int strikeBonus(int rollIndex) {
  36. return rolls[rollIndex + 1] + rolls[rollIndex + 2];
  37. }
  38. private boolean isStrike(int rollIndex) {
  39. return rolls[rollIndex] == MAX_PIN_NUM;
  40. }
  41. private boolean isSpare(int rollIndex) {
  42. return rolls[rollIndex] + rolls[rollIndex + 1] == MAX_PIN_NUM;
  43. }
  44. }

至此,一个功能正确,且代码整洁的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

发表评论

表情:
评论列表 (有 0 条评论,33人围观)

还没有评论,来说两句吧...

相关阅读

    相关 TDD测试驱动开发

    TDD(Test-Driven Development)是敏捷开发中的一项核心实践和技术,也是一种设计方法论,其基本思想是:在明确要开发某个功能后,在开发功能代码之前,