TDD基础-红绿重构练习之“井字游戏“

TDD基础-红绿重构练习之"井字游戏"

“红灯-绿灯-重构”流程是TDD的基石,以极快的速度在测试和实现代码之间切换。期间将失败,然后成功,然后重构,测试,整个过程不断重复,以此打造高质量代码和高质量产品功能。

1. 红绿重构过程

红绿重构源自代码在周期内的状态:处于红灯状态时,代码不管用;处于绿灯状态时,一切如预期一样工作,但并不一定是最佳;到了重构阶段,由于我们有良好的测试覆盖了各项功能,这个时候可以充满信心的去重构

1.1 编写一个测试

每次添加新功能时,先编写一个测试,这么做的目的是编写代码前专注于需求和代码设计,理清为了实现目标,需要设计什么样的功能,功能需要什么样的输入及输出。维护良好的测试亦是维护可执行的文档,在日后可以帮助理解代码目的和背后的意图。

在这个阶段,我们处于红灯状态,测试应该是不能通过的,如果测试通过反而预示着问题,测试本身设计不足以检查改功能或该功能已经被实现。

1.2 运行所有测试并确认新增测试未通过

运行所有测试的目的是确保已有的功能都没有被影响,确保最后一个未通过是由于当前依然处于红灯状态。该阶段由于是新增了一个测试,也可只运行当前新增测试,保证其不能通过。

1.3 编写实现代码

该阶段目标是使1.2种未通过的测试通过,该阶段的核心在于让测试通过,不必试图让代码完美(事实上,很难在第一次做功能实现时就可以实现的极其完美),不必花太多时间,也不必引入功能之外的设计,即便写的不好或不是最后的也没关系,后面还有改进的机会(后面不是指交付以后),这里的核心意图是打造一个由测试构成的安全基线,改不过不要引入新的功能,如果需要引入回到第一步,重新去考虑需求和代码设计,该阶段依然处于红灯。

1.4 运行所有测试

运行所有测试,保证新编写的代码不会破坏其他功能,过程中如果整个测试执行速度缓慢,就意味着测试设计的不好或者耦合度太高,耦合度高将导致难以隔离外部依赖,进而增加执行测试的时间。当所有测试通过后,这时就处于绿灯状态。

1.5 重构

这一步是可选的,虽然不是每个周期结束后都会进行重构,但迟早甚至必须会这么做(需求的迭代,复杂度的提升),没有明确的规定说什么时候该重构、什么时候不用重构。不过一旦认为可以用更佳或更优的方式重写代码,那就是重构的最佳时机。

什么样的代码需要重构呢?这个问题不好回答,因为重构的原因有很多:代码难以理解、代码位置不合理、代码重复、名称没有清晰阐述意图、方法太长、类的功能太多等——这个清单可 不断列下去。不管原因是什么,最重要的规则是重构不能改变任何既有功能

1.6 重复

以上步骤完成后,再重复它们,整个过程虽然看起来很长,很复杂,但实际并非如此,不知道大家是否有过在有单测保障的情况下,重构代码的体验,整个过程是非常流畅和快速的,红绿重构的周期应该类似,并且随着实践经验的丰富,整个周期将越来越短。保持快速前进,快速失败并更正,然后再重复是整个过程的关键。

2. 红绿重构练习-井字游戏需求

2.1 需求描述

井子游戏是一种儿童游戏,两个人轮流在一个3X3的网格中画X和O,最先在水平、垂直对角线上将自己的3个标记连起来的玩家获胜。这个练习中将拆分明确的需求,并根据需求编写测试,再编写满足测试期望的代码,最后如有必要进行重构,并且将重复这个过程,对同一需求编写多个测试,一个一个的实现所有需求。实际写代码时不会预先制定练习中详细的需求列表(不可能定义每一个需要的方法),而是直接编写用作需求和验证的测试(直接撸测试+实现)。另外练习中的实现只是众多实现路径中的一种。

2.2 开发井字游戏

2.2.1 需求1:首先定义边界、以及棋子放在哪里是合法,哪里是不合法的。

首先设计一个棋盘,定义一个井字游戏类TicTacToe,并使用3X3的二维数组模拟网格,添加play方法,模拟下棋。

public class TicTacToe {
   

    private int[][] board = {
   {
   0, 0, 0}, {
   0, 0, 0}, {
   0, 0, 0}};
    
    public void play(int x, int y) {
   

    }
}

根据需求可分成三个测试(验证输入参数是否合法):

  • 如果棋子放在超出了X轴边界的地方,就引发RuntimeException异常;
  • 如果棋子放在超出了Y轴边界的地方,就引发RuntimeException异常;
  • 如果棋子放在已经有棋子的地方,就引发RuntimeException异常。
按红绿重构流程,编写第一个测试
public class TicTacToeTest {
   

    private TicTacToe ticTacToe;

    @Before
    public void before() {
   
        ticTacToe = new TicTacToe();
    }

    @Test(expected = RuntimeException.class)
    public void whenOutXSideThenThrowRuntimeException() {
   
        ticTacToe.play(5, 2);
    }
}

给测试方法指定描述性名称,好处之一是有助于理解测试的目标,帮助掌握有些测试失败的原因以及在什么情况下增加测试可提高代码覆盖率,在测试方法名中,应明确指出测试前设置的条件、执行的操作以及期望的结果,给测试方法命名的方式很多,推荐BDD场景使用的given/when/then语法给它们命名,其中Given描述前置条件,When描述操作,而Then描述 期望的结果。如果测试没有前置条件(这些条件通常是使用注解@Before和 @BeforeClass设置的),则可省略Given,不要完全依靠注释指出测试的目标,因为使用IDE执行测试时,注释不会出现,它们也不会出现在CI工具或构建工具生成的报告中。

运行第一个测试

运行maven test,而不是在测试类上运行指定方法,以此保证所有测试都可以通过,实际项目中可能由于测试用例缺乏维护,可采用maven-surefire-plugin插件,指定得到维护的测试运行,该插件可生成txt和xml的测试用例包报告,如果想生成html的使用maven-surefire-report-plugin插件,如果需要更详细的测试覆盖率等信息,推荐使用jacoco,本例中运行后如下,红灯失败:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running TicTacToeTest
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.081 sec <<< FAILURE!
whenOutXSideThenThrowRuntimeException(TicTacToeTest)  Time elapsed: 0.007 sec  <<< FAILURE!
java.lang.AssertionError: Expected exception: java.lang.RuntimeException
        at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:34)
        at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
        at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
        at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
        at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
        at org.apache.maven.surefire.junit4.JUnit4Provider.
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值