什么是TDD,字面很好理解,曾经的我以为就是先写测试,然后写实现,接着跑测试,通过,收工。今天厚着脸皮抓到高级敏捷咨询师力岩同学一起Pair,告诉我什么是真正的TDD,他以一个小Case的练习把我带进了TDD的大门,下面就来总结一下他是怎么step by step TDD的。
需求:国际象棋,有一个车(Rook),一个国王(King),当国王和车处于一条横线或一条竖线的时候,就叫做将军(checkmate),现在就根据TDD来实现这个Story。
1、创建测试用例,
@Test
//注1:方法命名一定要清楚,代码即文档
public void should_rook_checkmates_king_when_rook_and_king_in_the_same_line() throws Exception {
//注2:完全按照自己的想法来写,比方说这儿,我们需要一个rook和一个king,并且要放到一个棋盘上才能判断是否将军,是吧?
ChessBoard chessboard = new ChessBoard();
King king = new King();
Rook rook = new Rook();
chessboard.addKing(king,3,4);
chessboard.addRook(rook,4,4);
assertThat(rook.isCheckmate(),is(true));
}
当然此时的代码肯定会有编译错误,这时,我们可以通过IDE的fix功能让编译通过先,记住,只是让编译通过,不要去想新加的类啊,方法的测试,设计什么的。不要偏离主航道,切记。
2、运行测试用例。经过step 1之后,我们会得到几个对象,和一些dummy的方法,因为是dummy的,很明显会报各种错误,NullPointer啊什么的。
3、以最简单的方法让测试通过,不要去考虑具体的实现逻辑实现,代码美观这些问题,专注于一个点,让测试通过,以我们这个测试为例,我们直接让rook的isCheckmate方法返回true就行了。
4、补测试用例,很明显,我们前面的方法很容易就绕过了测试的保护,说明我们的测试用例覆盖不够。接下来,加一个测试用例覆盖没有将军的情况
@Test
public void should_not_rook_checkmates_king_when_rook_and_king_is_not_in_the_same_line() {
ChessBoard chessboard = new ChessBoard();
King king = new King();
Rook rook = new Rook();
chessboard.addKing(king,3,3);
chessboard.addRook(rook,4,4);
assertThat(rook.isCheckmate(),is(false));
}
5、重跑所有测试用例,新补的这个测试用例不能通过。
6、修复失败的测试用例,专注于修复失败用例,怎么简单怎么来,这个Story下我们就需要修改rook的isCheckmate()方法。跟step 1一样,用最简单的方法使编译通过,这时的Rook类代码如下
public class Rook {
private King king;
private int x;
private int y;
public boolean isCheckmate() {
//注1:要判读是否将军,必须先知道King的位置吧
int king_x = getKing().getX();
int king_y = getKing().getY();
//注2:横坐标或者纵坐标相同,则将军
if(x == king_x || y==king_y){
return true;
}
return false;
}
public King getKing() {
return king;
}
}
同时,King类中也新增了2个dummy方法getX(),getY().
7、重跑所有测试,测试仍然不通过,继续fix测试,进过一系列的修改让测试通过,Rook的代码编程这样了
public class Rook {
private King king;
private int x;
private int y;
private ChessBoard chessboard;
public boolean isCheckmate() {
//注1:要判读是否将军,必须先知道King的位置吧
int king_x = getKing().getX();
int king_y = getKing().getY();
//注2:横坐标或者纵坐标相同,则将军
if (x == king_x || y == king_y) {
return true;
}
return false;
}
public King getKing() {
//注3:这个king的应该是从一个棋盘上扣下来的才有意义
return chessboard.getKing();
}
//注4:chessboard从哪儿来,给个setter让外部能传入,什么时候设呢?把rook放到棋盘的时候
public void setChessboard(ChessBoard chessboard) {
this.chessboard = chessboard;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
还有其他很多修改就不一一列出。
8、所有代码逻辑完成,测试通过,接下来就是Bad Smell的清理,以Rook类为例,很明显可以看出的Bad Smell 有一下几点
public class Rook {
//smell 1:删除无用变量
private King king;
//smell 2: x,y强耦合,可以内聚成一个变量
private int x;
private int y;
private ChessBoard chessboard;
public boolean isCheckmate() {
int king_x = getKing().getX();
int king_y = getKing().getY();
//smell 3:if条件的含义不明,可以抽出方法
//smell 4: 整个if语句可以inline成一个语句
if (x == king_x || y == king_y) {
return true;
}
return false;
}.....
记住每清理一个Bad Smell就运行一下测试,小步快跑,避免breakdown测试。
9、展示一下最后完成的Rook类
public class Rook {
private ChessBoard chessboard;
private ChessBoardPoint position;
public boolean isCheckmate() {
return isRookCheckmate();
}
private boolean isRookCheckmate() {
return position.getX() == getKing().getPosition().getX() || position.getY() == getKing().getPosition().getY();
}
public King getKing() {
return chessboard.getKing();
}
public void setChessboard(ChessBoard chessboard) {
this.chessboard = chessboard;
}
public void setChessboardPosition(ChessBoardPoint chessBoardPoint) {
this.position = chessBoardPoint;
}
}
当然这不是终点,只是起点,可能还有很多bad smell,这整个的TDD的过程中,看着代码一步一步的变优美是一个非常享受的过程。
总结: 今天的Pair,感受最深的是,专注,思想永远在一条线上,不要过多的去考虑当前不涉及的东西。