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

    技术2022-07-15  58

    文章目录

    使用Intellij来实践测试驱动开发 TDD Kata前言创建Java Maven项目TheBowlingGame KataThe RequirementsStep1: 创建项目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 8Maven 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-kataGroupId: cn.xdevops.kataArtifactId: 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.htmlhttps://www.jetbrains.com/help/idea/refactoring-source-code.html软件匠艺 Software Craftsmanship nklinsirui 认证博客专家 DevOps DevOps的实践者和布道者。现在某国际开源软件公司担任高级专家顾问。曾在某电子政务公司担任高级系统架构师。曾在某国际咨询公司担任技术架构交付经理。曾在某跨国银行中国软件开发中心担任高级专家顾问。
    Processed: 0.018, SQL: 9