-
代码在周期内的状态:处于红灯状态时,代码不管用,处于绿灯状态时,一切都想预期的那样工作,但并不一定是最佳的,到了重构阶段,我们知道测试很好的覆盖了各项功能,可以充满信息地修改他,让他变得更好
-
编写一个测试:
- 每次添加新功能时,都要首先编写一个测试,当前处于红灯状态,因为测试执行时以失败告终,即测试对代码的期望和代码实际的功能之间存在差距,更具体的说,没有代码满足最后一个测试的期望,因为我们还没有编写这样的代码,在这个阶段,可能所有的测试都通过了,但这表示存在问题
-
运行所有测试并确认最后一个未通过:
- 确认最后一个测试未通过后,就能断定他不会在没有引入新代码的情况下错误通过,如果这个测试通过了,就意味着要么相关功能早就存在,要么测试本身存在误报的问题,如果测试无论怎么实现都能通过,就意味着他毫无价值,应该删除。最后一个测试不仅必须未通过,还必须是预期原因导致的,在这个阶段,我们依然处于红灯状态,运行测试,但最后一个未通过
-
编写实现代码
- 这个阶段的目标是编写代码使最后一个测试通过。不要试图让代码完美无缺,也不要为编写花过多时间。即便编写的不好或者不是最后的,也没有关系,后面还有改进的机会。我们的真实意图是打造一个由测试构成的安全网,并确认这些测试都能通过。不要试图引入最后一个测试未描述的功能。要想引入新功能,必须回到第一步,先编写新测试。然而,仅当所有既有测试都通过后,我们才能这么做。在这个阶段,我们依然处于红灯状态。虽然已编写的代码可能让所有测试都通过,但这种假设还未得到证实。
-
运行所有测试
- 应运行所有测试,而不是只运行最后编写的那个测试,这至关重要。你刚编写的代码可能让最后一个测试得以通过,但同时破坏了其他功能。通过运行所有测试,不仅可确认最后一个测试的实现是正确的,还可确认它没有破坏整个应用程序的完整性。如果整个测试集执行速度缓慢,就昭示着测试编写得不好或者代码耦合度太高。耦合度太高将导致难以隔离外部依赖,进而增加执行测试所需的时间。在这个阶段,我们处于绿灯状态:所有测试都通过,且应用程序的行为符合预期
-
重构
- 前面所有步骤都是必不可少的,但这一步是可选的。虽然很少在每个周期结束后都进行重构,但迟早需要甚至必须这样做。并非每个测试的实现都需要重构;没有明确的规定说什么时候该重构、什么时候不用重构。一旦认为可以更佳或更优的方式重写代码,那就是重构的最佳时机。
- 什么样的代码需要重构呢?这个问题不好回答,因为重构的原因有很多:代码难以理解、代码位置不合理、代码重复、名称没有清晰阐述意图、方法太长、类的功能太多等——这个清单可不断列下去。不管原因是什么,最重要的规则是重构不能改变任何既有功能。
-
重复
- 所有步骤都完成后(其中重构是可选的),再重复它们。编写110行代码后就切换到下一步,因此整个周期的持续时间为几秒几分钟。如果更长,就说明测试的范围太大,应将其分成多个更小的测试。一定要快速前进,快速失败并更正,然后再重复。
-
案例:3*3的棋盘下棋
-
实现
public class TicTacToeSpec { @Rule public ExpectedException exception = ExpectedException.none(); private TicTacToe ticTacToe; @Before public final void before() { ticTacToe = new TicTacToe(); } @Test public void whenXOutsideBoardThenRuntimeException() { exception.expect(RuntimeException.class); ticTacToe.play(5, 2); } }
-
expected属性,你可以用它来指定一个Throwble类型,如果方法调用中抛出了这个异常,这条测试用例就算通过了
-
这个测试中,我们指出调用方法ticTacToe.play(5,2)时,期望的结果是引发RuntimeException异常,这个测试只需创建方法play,并确保他在参数x小于1或者大于3时引发RuntimeException异常
-
应该测试三次,第一次运行时,他应该不通过,因为此时还没有方法play,添加这个方法后,测试也应该不通过,因为他没有引发异常RuntimeException,第三次运行时应该通过,因为他实现了与这个测试相关联的所有代码
-
-
测试
-
验证Y轴
-
@Test public void whenYOutsideBoardThenRuntimeException() { exception.expect(RuntimeException.class); ticTacToe.play(2, 5); }
-
-
实现
-
public void play(int x, int y) { if (x < 1 || x > 3) { throw new RuntimeException("X is outside board"); } else if (y < 1 || y > 3) { throw new RuntimeException("X is outside board"); } }
-
为让最后一个测试通过,添加一条“检查参数Y是否在棋盘内”的else子句。
-
-
测试
-
确定棋子在棋盘边界内后,还需确保它放在未被别的棋子占据的地方
-
@Test public void whenOccupiedThenRuntimeException() { ticTacToe.play(2, 1); exception.expect(RuntimeException.class); ticTacToe.play(2, 1); }
-
-
实现
-
为实现最后一个测试,应将既有棋子的位置存储在一个数组中。每当玩家放置新棋子时,都应确认棋子放在未占用的位置,否则引发异常
-
private Character[][] board = {{'\0', '\0', '\0'}, {'\0', '\0', \0'}, {'\0', '\0', '\0'}}; public void play(int x, int y) { if (x < 1 || x > 3) { throw new RuntimeException("X is outside board"); } else if (y < 1 || y > 3) { throw new RuntimeException("Y is outside board"); } if (board[x - 1][y - 1] != '\0') { throw new RuntimeException("Box is occupied"); } else { board[x - 1][y - 1] = 'X'; } }
-
-
重构
-
重构play方法
-
public void play(int x, int y) { checkAxis(x); checkAxis(y); setBox(x, y); } private void checkAxis(int axis) { if (axis < 1 || axis > 3) { throw new RuntimeException("X is outside board"); } } private void setBox(int x, int y) { if (board[x - 1][y - 1] != '\0') { throw new RuntimeException("Box is occupied"); } else { board[x - 1][y - 1] = 'X'; } }
-
-
-
编写一个测试,发现他不能通过,编写实现代码,发现所有测试都通过,只要有机会就重构代码使其变得更好,并重复这个过程