- 我不能放棋子到已被占用的棋位置上。这个需求是个验证性需求,要保证棋子不能重叠和覆盖已在棋盘上的棋子,实现这个需求我只要重构现有的代码加上避免棋子重叠的逻辑。只要避免在 PutChess 时候,检查是否指定的位置是否已有棋子,如果是简单的抛出异常即可。有了这些基本的思路,我开始设计测试用例。
1
2
3
4
5
6
7
8
9
10
| TEST_F(TicTacToeTestFixture,BizException_Occupied){
IGameBoard *gameBoard=new SimpleGameBoard("simple board");
char xChar='X',yChar='0';
EXPECT_NO_THROW(gameBoard->utChess(0,0,xChar));
EXPECT_THROW(gameBoard->utChess(0,0,xChar),ChessOverlapException);
EXPECT_NO_THROW(gameBoard->utChess(2,2,yChar));
EXPECT_THROW(gameBoard->utChess(2,2,yChar),ChessOverlapException);
delete gameBoard;
}
|
ChessOverlapException 是我将要实现的一个异常类,这个是在棋手试图放棋子到已有棋子的棋盘位置上时要抛出的异常。测试用例中,我在(0,0)和(2,2)这两个位置上放同样的棋子以触发这个异常。为了编译通过,我开始实现 ChessOverlapException。 ChessOverlapException 继承自 std::exception 我重载了 what 函数返回相应的异常信息。 把这个异常类的定义引入的测试工程中,编译通过运行测试,但却得到了红色 Red,案例失败:
图 6.测试用例输出点击查看大图
原因是我还没有重构 PutChess 函数以加入避免棋子被被覆盖的代码。现在来重构 PutChess 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| void SimpleGameBoard:utChess( int x,int y,char chess )
{
assert(x<xMaxDim&&y<yMaxDim);
int xy=x*3+y;
if(data_.size()==0){
initboard_();
data_[xy]=chess;
return ;
}
if(data_[xy]!='+') {
throw ChessOverlapException("chess overlap!");
}
else data_[xy]=chess;
}
|
重新编译测试工程并运行得到绿色 Green 通过。继续下一个需求。
- 我要能判断是不是棋盘已满并无赢家。 这个需求用于判断是否是和棋的情况,棋盘满了但并无赢家,这是可能出现的一种情况,这个实现设计可以有两种方式. 一是重构 CheckWinOut 函数,使返回值携带更多的信息,比如和棋,有人胜出等。二是定义一个独立的函数去判断棋盘的当前状态。第一种方案较合理,开始设计这种方案的测试用例:
1
2
3
4
5
6
7
8
9
10
11
12
| EST_F(TicTacToeTestFixture,IsEndedInADraw)
{
char xChess='X',yChess='O';
IGameBoard *gameBoard=new SimpleGameBoard("simpleBoard");
gameBoard->utChess(0,0,yChess);gameBoard->utChess(0,1,xChess);gameBoard->utChess(0,2,yChess);
gameBoard->utChess(1,0,xChess);gameBoard->utChess(1,1,yChess);gameBoard->utChess(1,2,yChess);
gameBoard->PutChess(2,0,xChess);gameBoard->PutChess(2,1,yChess);gameBoard->PutChess(2,2,xChess);
GameBoardStatus status=gameBoard->CheckWinOut(yChess);
EXPECT_TRUE(status==GAMEDRAW); <br>GameBoardStatus status2=gameBoard->CheckWinOut(xChess); EXPECT_TRUE(status2==GAMEDRAW);
delete gameBoard;
}
|
以上的测试用例可以看出, 我设计了和棋的棋局,并想重构 CheckWinout 函数,使其返回枚举类型 GameBoardStatus 以表示棋局的状态,其中 GAMEDRAW 表示和棋状态。为了使工程能编译通过,开始定义这个枚举类型并重构 CheckWinOut 函数。实现所有设计,经过几次的 Red 失败,最终 形成代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
| GameBoardStatus SimpleGameBoard::CheckWinOut(char chess)
{
if(IsThreeInLine_(chess)){
return GAMEMWINOUT;
}
else if(IsEndedInADraw_()){
return GAMEDRAW;
}
else{
return GAMERUNNING;
}
}
|
其中那个 IsEndedInADraw_是个受保护的成员函数,用于检测是否和棋。 在调通这个测试用例的过程中,我也更新了测试JugeThreeInLine。因为重构 ChecWinOut 改变了返回类型。
- 我需要能复位棋盘,以便于重新开始下棋。
- 我需要用对记住玩家,以便于我能特例化 Player。6 和 7 需求的测试案例和实现比较比较简单,不在赘述,7 的要求是要建立玩家 Player,这个主要是说要能实例化玩家。可以看附带的工程。
- 我需要能保存和加载棋局能力,以便于我能下次回来继续之前的游戏。这个需求是一个合理的需求,玩家可以保存和继续回来玩游戏,他的测试用例可以这样设计:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| TEST_F(TicTacToeTestFixture,SaveTheBoard)
{
IGameBoard * gameBoard=new SimpleGameBoard("simpleBoard");
char xChess='x',yChess='o';
gameBoard->PutChess(0,0,xChess);
gameBoard->PutChess(1,2,yChess);
IGameIO *gameIO=new SimpleGameIO();
EXPECT_NO_THROW(gameIO->save(gameBoard,"somewhere"));
delete gameBoard;
delete gameIO;
}
TEST_F(TicTacToeTestFixture,LoadTheBoard)
{
IGameBoard * gameBoard=new SimpleGameBoard("simpleBoard");
char xChess='x',yChess='o';
gameBoard->PutChess(0,0,xChess);
gameBoard->PutChess(1,2,yChess);
IGameIO *gameIO=new SimpleGameIO();
EXPECT_NO_THROW(gameIO->save(gameBoard,"somewhere"));
IGameBoard *game=gameIO->load("somewhere");
EXPECT_EQ(xChess,game->GetChess(0,0));
EXPECT_EQ(yChess,game->GetChess(1,2));
EXPECT_EQ('+',game->GetChess(2,2));
delete game;
|
1
2
3
| delete gameBoard;
delete gameIO;
}
|
这里用两个测试用例来覆盖这个需求,一个是保存棋盘,一个是加载棋盘。由这个测试用例可以看到,要通过这个测试,必须要定义 IGameIO 接口和 SimpeGameIO 类。 保存棋盘的媒介是文件。按照 TDD 的开发要求,测试单元本身最好是脱离对第三方系统的依赖,但测试中必然会用到第三方系统,解决这些问题的方法有几种。创建第三方系统的 Stub 类或是 FakedObject,第三种选择是 Mock 框架,如 Gmock。 Gmock 的设计理念是基于接口的,只要是第三方访问提供的是接口,这些访问就可以可以被用 Gmock 模拟。可以看参考文献获取更多的信息。 限于篇幅不再赘述。一下是完成所有测试用例的测试结果。
图 7.测试用例输出或许你会注意到有些测试用例的设计,只是以点盖面,如果想要更多的验证点可以借助于 Gtest 提供的参数化测试设计测试数据,然后去测试实现的类和逻辑。 还有死亡测试的用例,可以在参考资源中的 Gtest 资源中查看。
|