摘要:用测试的方法驱动开发,这个概念的提出已经很长时间了,但测试驱动开发在 C 和 C++的应用和实践却比较晚,本文用一个简单项目的实例说明如何在 C 和 C++的开发过程中,应用测试驱动开发的理念,从需求定义,代码测试案例设计到开发实现这些案例定义的需求,展现了测试驱动开发的魅力。测试驱动开...

单元测试框架比较和筛选

C++技术是一种高级语言,它出现的时间要比 Java 和 C#早得多,但支持像 xUnit 框架的 C++单元测试框架发展起来的比较晚。 C++ 的单元测试框架选择比较多,现在比较流行的 C++测试框架有 Boost Test、UnitTest++、CppTest、Google C++ Testing Framework。 Boost Test,拥有良好的断言功能,对异常控制,崩溃控制方面处理的比较好,也有良好的可以移植性,但结构复杂,不易于掌握。CPPUnit 是开发比较早的单元测试框架,是对 JUnit 的 C++的移植的一种尝试,拥有丰富的断言和期望功能。Google Test C++ 简称 Gtest,是近期发展起来的单元测试框架,对 xUnit 支持的比较好,支持 TDD 的红-绿-重构模式,支持死亡和退出测试,较好的异常测试控制能力,良好的测试报告输出,拥有自动注册测试用例和用例分组等功能,还有和 Gmock 框架的无缝结合,支持基于接口的(抽象类的)Mock 测试-模拟测试。

下表是一个对三种流行 C++单元测试框架的简单比较,Gtest 虽然发展起来的较晚,但丰富功能简单易用,易学,加之移植性较好,是跨平台项目单元测试框架比较好的选择。

表 1.单元测试框架比较
测试框架支持特性 Gtest Boost Test CPPUnit
可移植性 较好 好(依赖于 Boost 库) 较好
丰富的断言 一般
丰富的断言信息 良好 较差
自动检测和注册测试用例 一般
易于扩展断言 易于扩展 一般 一般
支持死亡和退出测试(Death 和 Exit) 支持 支持 不支持
支持参数化测试(Parameterized test) 支持 支持 不支持
支持 Scoped_Trace 支持 不支持 不支持
支持选择性执行测试用例 支持 支持 支持
丰富的测试报告形式(xml) 支持 支持 支持
支持测试用例分组 Suites 支持 支持 支持
开源
执行速度
基于接口的Mock测试 通过Gmock支持 不支持 不支持
易用性 优秀 较复杂 较好
支持类型化的参数化测试 支持 不直接支持 不直接支持

 

 

TEST_F(TicTacToeTestFixture,IWantAGameBoard) { IGameBoard *gameBoard=NULL; EXPECT_NO_THROW(gameBoard=new SimpleGameBoard("simpleGame")); EXPECT_TRUE(gameBoard!=NULL); EXPECT_NO_THROW(delete gameBoard); }

 

这是第一个测试用例,稍微解释一下。TicTacToeTestFixture 是用于测试的分组的,它是一个类,继承于 Gtest 的 test 类 testing::Test,这个类可以重载 setup 和 teardown 等虚拟函数用于测试准备和清理测试现场。TEST_F 是定义测试用例的宏,IWantAGameBoard 是测试的案例的名称,会显示在输出中,测试用例很简单,只是只是保证能创建和析构 SimpleGameBoard 实例,并无异常抛出。这个测试用例现在是不能编译通过的,因为 IGameBoard 接口和 SimplegameBoard 都还没有声明和定义,接下来为了使这个案例通过,我在 TicTacToeLib 工程里,声明和定义 IGameBoard 和 SimpleGameBoard 类,IGameBoard 是纯抽象类,抽象了所有对棋盘的操作。引入声明到测试工程中,编译通过并运行,现在完成了第一测试用例,尽管测试的 IGameBoard 和 SimpleGameBoard 还是空的。可以看一下输出:

图 4 .测试用例输出

面向 C++ 的测试驱动开发_TDD

  • 我需要在棋盘上下棋和获取到棋子

    这个需求能使棋手在棋盘上把棋子放到想要的位置上并能查看指定棋盘位置上的棋子,棋盘是 3x3。实现这个需求也很简单,我只要在 IGameBoard 接口上添加两个函数然后在 SimpleGameBoard 里实现这两个函数就可以满足这个需求:

     

    TEST_F(TicTacToeTestFixture,JugeThreeInLine) { IGameBoard *gameBoard=new SimpleGameBoard("simpleBoard");  IGameBoard *gameBoard2=new SimpleGameBoard("simpleboard2");  char xChess='x',yChess='o';  gameBoard->PutChess(0,0,xChess); gameBoard2->PutChess(0,1,yChess);  gameBoard->PutChess(1,1,xChess); gameBoard2->PutChess(1,1,yChess);  gameBoard->PutChess(2,2,xChess); gameBoard2->PutChess(2,1,yChess);  EXPECT_TRUE(gameBoard->CheckWinOut(xChess)); EXPECT_TRUE(gameBoard2->CheckWinOut(yChess));  EXPECT_FALSE(gameBoard->CheckWinOut(yChess)); EXPECT_FALSE(gameBoard2-)CheckWinOut(xChess));  delete gameBoard;  delete gameBoard2;  }

     

    设计是这样的,为简单,我把判断棋子胜出的函数 CheckWinOut 定义到接口 IGameBoard 中,并在 SimpleGameBoard 中实现它,实现如下:

     

    void SimpleGameBoard::PutChess( 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 函数,使返回值携带更多的信息,比如和棋,有人胜出等。二是定义一个独立的函数去判断棋盘的当前状态。第一种方案较合理,开始设计这种方案的测试用例:

     

    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;