java测试驱动编程简明教程
本篇教程基于junit5,不熟悉的人可以先去官网了解下:https://junit.org/junit5/ 当然比较推荐我个人整理的junit5的简明教程,可以帮你省下不少时间
测试驱动编程实战
我们的原始需求
开发一个“井”字游戏,双方在3X3的网格中画X和O,最先在水平、垂直、对角线上将自己3个标记连起来的玩家获胜
- 初始用户故事:作为玩家,可以将棋子放在3X3棋盘上任何没有棋子的地方
故事验收:
- 1 如果棋子放在超出X轴边界地方,引发RuntimeException;
- 2 如果棋子放在超出Y轴边界地方,引发RuntimeException;
- 3 如果棋子放在已经有棋子的地方,引发RuntimeException
开始红-绿-重构
- 首先我们在业务模块创建一个井字游戏的实现类 JingGameService
- 然后利用Idea工具创建对应的测试类(手动也可以,这里只是图个方便)
选择创建一个在每个测试方法之前都执行的方法
- 添加一个初始化井字游戏实例的方法
- 编写X轴越界的测试方法
注意,我们创建的井字游戏类还没有任何的方法,所以肯定会报错的,这个过程就是红-绿-重构
过程中的红
阶段
- 为了进入绿阶段我们实现
play
方法
-
执行下该测试方法
我们看到测试通过了,这个过程就是红-绿-重构
过程中的绿
阶段
- 我们还可以查看下Junit5自动生成的测试报告
-
此时我们的代码比较简单也没有重构的必要,继续完成其他的验收测试
- Y轴的异常逻辑和X的如同一辙,我们马上就可以实现
执行下整个类的测试,发现都是绿的,此时我们又经过了一次红-绿-重构
的过程(重构没做?因为目前还不用嘛)
- 好了,只剩下最后一个验收测试了:确定棋子在棋盘内,并且该位置是没有棋子的
- 先完成位置冲突的测试用例
执行下,不出意外的报错了:
- 接下来进入
绿
阶段,进行业务代码的编写
执行下整个测试类,全部通过:
- 此时的代码其实已经可以重构了,因为我们的判断语句比较多,不能很好的表达业务含义,并且这些判断可能其它地方也会用到,应该抽取为不同的方法,用明确的含义表示出来该步骤的意图
下面是一个代码重构的参考:
重构完记得重新执行下测试类哦
PS:使用Idea重构很方便,只要二步
第一步:选中要重构的代码
第二步:起个方法名和选择需要的参数
- 好了到目前为止,需求1的用户故事结束了
进入需求2
我们得加入处理玩家落子的规则,验收测试如下:
- 白子先下
- 如果上一次是白子,那么这次是黑子
- 如果上一次是黑子,那么这次是白子
我们开始红-绿-重构
的过程:
先编写测试方法,当白子先下,下一个玩家应该是白,我们用W表示白子,B表示黑子
红
的过程完成了,接下来是绿
的过程,编写业务方法
我们只要编写一行代码就能实现了,然后继续完成白方黑方交替下的过程
编写白方完成后轮到黑方测试方法:
修改刚才的nextPalyer方法,将它变绿:
因为我们修改了nextPalyer方法,之前的用例也要重新跑一次,保证一直是绿的
那么我们需要编写黑方下完后应该是白方的逻辑吗?如果你写完测试方法,发现业务代码啥也不用动就能正确通过了,那么说明这个测试是多余的,我们应该删除掉
OK,我们又完成了第二个需求
进入需求3
终于进入该游戏的关键规则环节了:如何获胜?最先在水平、垂直、对角线上将自己3个标记连起来的玩家获胜
我们需要检查三个方向:水平,垂直,对角线,当没有分出胜负时,我们也要返回没有赢家,先来完成这个
看来我们又要重构/修改play方法了
之前我们是使用0或1来表示是否有棋子的,现在不仅要知道某个位置上是否有棋子,还要知道棋子是谁下的,所以保存的时候直接保存是谁下的
继续编写垂直方向的
写了水平的,垂直的也就简单了
最后是对角线三连,注意对角线有二种可能
因为都只有唯一的一条线,判断起来也更加简单了
最后所有的用例跑一次看看
完美,所有的用例都通过了,需求3也完成了
进入需求4
最后我们要处理下平局,毕竟这个游戏的大多数结局可能是平局
平局只有一种可能,就是棋盘满了,所以最后的验收测试是,当棋盘满了,游戏显示为平局
进入红
阶段
进入绿
阶段
最后看下测试结果:
好了,一个超级简单的井字游戏就告一个段落了,代码层面还有一些优化的地方,比如获胜的判断这块。当然这不是本教程的主题,我主要演示的是如何通过测试驱动来进行业务需求/用户故事的开发
我们可以清晰的体会到,我们在编写的时候没有一开始就写业务代码,而是分析需求的验收测试,然后将验收测试编程Junit5的测试代码,当然这个过程中,代码肯定是会报错的,报错的原因有二类:
- 编译错误:缺少必要的方法和参数
- 运行错误:逻辑上的不满足
我们要做的事情就是把不通过的测试用例都变绿,如此反复进行。当我们发现代码臃肿难以阅读时,就应该及时的进行重构,我们可以放心的进行重构,因为有之前的测试为我们检验重构后的结果,让我们不用过于担心代码重构的不可控。
测试覆盖率
最后我们看下Junit5提供的测试覆盖率功能:
在Idea的右上角可以看到下图所示,类、方法、代码行的测试覆盖率情况,让我们做到心里有数,有多少代码或方法是测试过的
一些忠告
- 测试方法的名称:使用given(前置条件,一般如果有公共的前置,方法中可省略)/when(描述操作)/then(描述期望的结果)
- 红-绿-重构的过程:保证短节奏式的交替进行
最后的话
本文讲述的只是一个非常简单的测试驱动需求例子,现实情况中需求比较复杂。我们需要转变的是我们开发系统的思想,一开始可能你还不是很熟练,但是随着练习次数,你将会喜欢上这种方法。就像当初学习DDD的时候,上手比较困难,各种概念难以理解,但是一旦你上手后,你再也不想回到以前那种贫血失血模型的开发中去了。
如果你立志成为一个优秀的工程师,那么测试驱动编程是你一定要掌握好的技能。
PS:本教程的源码分享在了 https://gitee.com/hzqiuxm/tdd-demo-lessons.git 的jingame模块