51Testing软件测试论坛

标题: Java 测试驱动开发--“井字游戏” 游戏实战 [打印本页]

作者: always_fly    时间: 2018-3-6 16:03
标题: Java 测试驱动开发--“井字游戏” 游戏实战
TDD 介绍

TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,
也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需
要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同
样可以适用于其他开发方法和过程。

-- 百度百科

准备工具

TDD只是一种开发模式,它并没有用到新的技术。

Java : 因为它是主流的编程语言,应用广泛,相关实践也非常多。

IntelliJ-IDEA : Java 主流IDE(集成开发工具)。

JUnit : Java 主流单元测试框架,当然,你选择 TestNG 也是完全可以的。

Gradle : 构建工具。

TDD 开发模式

“红灯 -- 绿灯 -- 重构” 流程是TDD的基石。 这个过程就像打乒乓球,快速的在测试代码和实现代码之间
切换。

TDD 开的过程: 每次只考虑一个需求。首先编写一个测试,看看它是否未通过;然后编写实现这个测试
的代码,运行所有测试并验证它们是否全部通过;最后,通过重构改进代码。不断重复这个过程,直到
成功实现所有需求。

需求

本系列实战 “井字游戏” ,这是一个非常简单的小游戏。
[attach]111444[/attach]

说明:

是一种在3*3格子上进行的连珠游戏,和五子棋比较类似,由于棋盘一般不画边框,格线排成井字故
得名。游戏需要的工具仅为纸和笔,然后由分别代表O和X的两个游戏者轮流在格子里留下标记(一
般来说先手者为X)。由最先在任意一条直线上成功连接三个标记的一方获胜。

不会玩的同学可以先去完两把:井字小游戏。



需求1
先定义边界,以及将棋子放在哪些地方非法。

可将棋子放在3×3棋盘上任何没有棋子的地方。

将需求分成三个测试:

如果棋子放在超出了X轴边界的地方,就引发 RuntimeException 异常。
如果棋子放在超出了Y轴边界的地方,就引发 RuntimeException 异常。
如果棋子放在已经有棋子的地方,就引发 RuntimeException 异常。


测试用例 1

默认你已经会使用 JUnit 单元测试框架了,根据上面的三个测试,我们先来完成第一个。
  1. import org.junit.Before;
  2. import org.junit.Rule;
  3. import org.junit.Test;
  4. import org.junit.rules.ExpectedException;


  5. public class TicTacToeTest {

  6.     @Rule
  7.     public ExpectedException exception =  ExpectedException.none();

  8.     private TicTacToe ticTacToe;

  9.     @Before
  10.     public final void before() {
  11.         ticTacToe = new TicTacToe();
  12.     }

  13.     @Test
  14.     public void whenXOutsideBoardThenRuntimeException() {
  15.         exception.expect(RuntimeException.class);
  16.         ticTacToe.play(5, 2);
  17.     }

  18. }
复制代码
测试调用 TicTacToe 类的 play() 方法,假设第一个参数是 x 轴,第二个参数是 y 轴,前面需求已经
规定,棋盘是3×3的规格,所以参数必须不能小于1或大于3。
x 轴为5会引发异常。在 whenXOutsideBoardThenRuntimeException() 测试用例中,预期这被测代码
会抛出 RuntimeException异常。

实现功能 1

接下来,我们要实现功能代码了,以满足测试用例通过。
  1. public class TicTacToe {


  2.     public void play(int x, int y) {
  3.         if (x < 1 || x > 3) {
  4.             throw new RuntimeException("X is outside board");
  5.         }
  6.     }
  7. }
复制代码
实现代码非常简单,创建TicTacToe 类和 play() 方法,判断 x 参数,如果小于1或大于3 将抛出 Runt
imeException异常。

** 现在再次执行 测试用例 1 检查它是否运行通过。

测试用例 2

继续在 TicTacToeTest 测试类中创建将的测试用例。

  1. @Test
  2. public void whenYOutsideBoardThenRuntimeException(){
  3.     exception.expect(RuntimeException.class);
  4.     ticTacToe.play(2,5);
  5. }
复制代码
这条用例用于验证棋盘 y 轴范围抛 RuntimeException 异常。

实现功能 2

继续修改 TicTacToe 的功能代码。使 测试用例2 运行通过。
  1. public class TicTacToe {


  2.     public void play(int x, int y) {
  3.         if (x < 1 || x > 3) {
  4.             throw new RuntimeException("X is outside board");
  5.         }else if(y < 1 || y > 3){
  6.             throw  new RuntimeException("Y is outside board");
  7.         }
  8.     }

  9. }
复制代码
  1. 实现功能 2

  2. 继续修改 TicTacToe 的功能代码。使 测试用例2 运行通过。

  3. public class TicTacToe {


  4.     public void play(int x, int y) {
  5.         if (x < 1 || x > 3) {
  6.             throw new RuntimeException("X is outside board");
  7.         }else if(y < 1 || y > 3){
  8.             throw  new RuntimeException("Y is outside board");
  9.         }
  10.     }

  11. }
复制代码
这里针对 play()方法,增加对参数 y 的判断,如果小于1或大于3则抛出RuntimeException异常。

** 现在再次执行 测试用例 2 检查它是否运行通过。

** 另外,保证 测试用例 1 也是可以运行通过的。



测试用例 3

继续在 TicTacToeTest 测试类中创建将的测试用例。
  1. ……

  2. @Test
  3. public void whenOccupiedThenRuntimeException(){
  4.     ticTacToe.play(2,1);
  5.     exception.expect(RuntimeException.class);
  6.     ticTacToe.play(2,1);
  7. }
复制代码
如果棋盘上的格子已经被占用,那么不允许再放子上去。

实现功能 3

为了实现测试用例3 ,应该将棋子的位置存储在一个数组中。每当玩家放置新棋子时,都应确认棋子
放在未占用的位置,否则引发异常。
  1. public class TicTacToe {


  2.     private Character[][] board = {
  3.             {'\0','\0','\0'},
  4.             {'\0','\0','\0'},
  5.             {'\0','\0','\0'}
  6.     };

  7.     public void play(int x, int y){
  8.         if(x < 1 || x > 3){
  9.             throw new RuntimeException("X is outside board");
  10.         }else if(y < 1 || y > 3){
  11.             throw  new RuntimeException("X is outside board");
  12.         }
  13.         if(board[x-1][y-1] != '\0'){
  14.             throw new RuntimeException("Box is occupied");
  15.         }else {
  16.             board[x-1][y-1] = 'X';
  17.         }
  18.     }

  19. }
复制代码
检查放置棋子的位置是否被占用,如果未占用,就将相应数组元素的值从空(\0)改为占用(X),
注意, 我们还没有记录棋子是谁(X 还是 O)的。

** 再次执行 测试用例 1、2、3 ,使它们全部运行通过。



重构

虽然 TicTacToe 代码已经满足了测试的需求,但是有点令人迷惑。所以需要对现有的代码进行重构。
  1. public class TicTacToe {


  2.     private Character[][] board = {
  3.             {'\0','\0','\0'},
  4.             {'\0','\0','\0'},
  5.             {'\0','\0','\0'}
  6.     };

  7.     public void play(int x, int y){
  8.         checkAxis(x);
  9.         checkAxis(y);
  10.         setBox(x, y);
  11.     }

  12.     private void checkAxis(int axis){
  13.         if(axis <1 || axis > 3){
  14.             throw new RuntimeException("X is outside board");
  15.         }
  16.     }

  17.     private void setBox(int x, int y){
  18.         if(board[x-1][y-1] != '\0'){
  19.             throw new RuntimeException("Box is occupied");
  20.         }else {
  21.             board[x-1][y-1] = 'X';
  22.         }
  23.     }

  24. }
复制代码
最后

本系列中的例子,来自《Java 测试驱动开发》 一书,我这几天在看,本身不厚,两百多页,内容也
不虚,我很讨厌,概念讲的高大上,例子只贴关键代码,这种书根本没法下手操作。而这本书中的例
子是可以跟着敲下来的,虽然代码有些小错误。如果感兴趣的同学,可以买正版支持一下,我自己写
书,所以知道写书的辛苦。

说说TDD 的感受,就是很爽!

看到每次测试用例全部通过很爽!!

重构代码很爽!! 因为有测试用例这一牢靠的后盾。
[attach]111445[/attach]


作者: 海海豚    时间: 2018-3-6 17:02
谢谢分享~
作者: tiantian010    时间: 2018-10-17 13:24
顶顶......!




欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2