面向 C++ 的测试驱动开发.docx
- 文档编号:13492058
- 上传时间:2023-06-14
- 格式:DOCX
- 页数:16
- 大小:104.64KB
面向 C++ 的测试驱动开发.docx
《面向 C++ 的测试驱动开发.docx》由会员分享,可在线阅读,更多相关《面向 C++ 的测试驱动开发.docx(16页珍藏版)》请在冰点文库上搜索。
面向C++的测试驱动开发
面向C++的测试驱动开发
用测试的方法驱动开发,这个概念的提出已经很长时间了,但测试驱动开发在C和C++的应用和实践却比较晚,本文用一个简单项目的实例说明如何在C和C++的开发过程中,应用测试驱动开发的理念,从需求定义,代码测试案例设计到开发实现这些案例定义的需求,展现了测试驱动开发的魅力。
测试驱动开发和现在流行敏捷开发的是分不开的,测试驱动开发是敏捷开发的一个强有力工具,可以帮助我们从简单的设计开始,逐步地有保护重构设计直至完善设计的过程。
0
评论:
夏建东,高级软件工程师,IBM
何凯,软件工程师,IBM
尹鹏,部门经理,IBM
2014年1月20日
∙
内容
测试驱动开发(TDD)背景及综述
测试驱动开发是Kent提出的一种新的软件开发流程,现在已广为人知,这种开发方法依赖于极短重复的开发周期,面对开发需求,开发人员要先开发代码测试用例,这些代码实现的测试用例定义了工程要实现的需求,然后去开发代码快速测试通过这这些用例,这个时候的代码是相对比较粗糙的,只是为了通过这个测试,测试通过以后,这些测试所覆盖的需求就会相对固定下来了,然后随着实现更多的需求,以前实现的那些粗糙的代码的问题会逐步的暴露出来,此时就要用重构来消除重复改进代码设计,因为自动化的测试用例已经框定了相应的需求,这样在代码改进和重构的过程中就不会破坏已实现的需求,实现了安全重构。
从测试驱动开发的流程可以看出来,测试驱动开发仅仅要求一个简单的设计开始实现需求,然后随着软件开发的推进实现有保护重构代码和设计。
依赖于TDD开发所生成的单元测试用例代码,实现有保护重构是大型的软件开发项目不可以缺少的,代码级别的测试更能有效地提高软件产品的质量。
测试驱动开发中的重构过程也是一个使设计逐步完善的过程。
本文的主要目的是使测试驱动开发落到实地,和具体的语言(C++)和单元测试框架结合起来,并用实例展示测试驱动开发的魅力。
回页首
测试驱动开发的信条
先开发和设计测试代码,再代码实现通过测试,以测试驱动设计实现,开发和设计的过程,得到了快速的反馈,用这些反馈驱动,改进和重构代码设计,是一个有机的开发过程。
按照Kent的定义,测试驱动开发的原则是:
1.不要写一行代码,除非有一个失败的自动化测试案例要纠正。
2.消除重复的代码,改进设计。
这两个简单的原则,却产生了一些复杂的个体和组的行为,这些隐含的技术行为包括:
∙运行代码对设计决定快速反馈下,实现有机地设计
∙必须自己写自己的测试用例,而不是等待别人帮你写测试代码,那样会花费很长时间
∙必须要有对变更代码快发反应开发环境
∙组件必须要高内聚、低耦合,以使测试简单化。
两个原则还隐含开发任务的顺序:
1.红色(Red):
写些不能够工作的小测试,这个测试甚至不能编译通过。
2.绿色(Green):
快速编写代码使测试通过,不用太在意代码质量只是通过测试。
3.重构(Refactor):
消除开始是只是要通过测试的重复代码,改进设计。
红色(Red)-绿色(Green)-重构(Refactor),这个就是测试驱动开发的座右铭(Mantra)。
这种开发方式可以有效的减少代码的缺陷密度,减少bug的数量,将大部分的缺陷在代码的开发过程中消除,减少了QA测试和质量保证的成本。
按照软件工程的说法,软件缺陷和bug发现的越早,所需的更正这些缺陷的成本就会越小。
所以在软件的开发阶段,采用测试驱动的开发方法,把测试引入到开发阶段,使测试和质量意识融入到开发的过程中,这对提高软件工程质量非常有帮助。
而且在采用测试驱动开发必然要求所开发的组件、接口、类或方法是可测试的(testable),这就要求开发的组件,接口要遵循组件和类高内聚(HighlyCohesive),组件和组件、类和类之间低耦合(looselyCoupled)原则,这种开发方式生成的代码必然会帮助开发者,在不断的有保护重构的过程中,提高软件架构的设计,使日后的软件维护变得有章可循。
测试驱动开发符合敏捷软件开发的精神,在不断迭代过程中,增量地实现软件需求而这一切开始可以从简单设计开始。
回页首
单元测试框架比较和筛选
C++技术是一种高级语言,它出现的时间要比Java和C#早得多,但支持像xUnit框架的C++单元测试框架发展起来的比较晚。
C++的单元测试框架选择比较多,现在比较流行的C++测试框架有BoostTest、UnitTest++、CppTest、GoogleC++TestingFramework。
BoostTest,拥有良好的断言功能,对异常控制,崩溃控制方面处理的比较好,也有良好的可以移植性,但结构复杂,不易于掌握。
CPPUnit是开发比较早的单元测试框架,是对JUnit的C++的移植的一种尝试,拥有丰富的断言和期望功能。
GoogleTestC++简称Gtest,是近期发展起来的单元测试框架,对xUnit支持的比较好,支持TDD的红-绿-重构模式,支持死亡和退出测试,较好的异常测试控制能力,良好的测试报告输出,拥有自动注册测试用例和用例分组等功能,还有和Gmock框架的无缝结合,支持基于接口的(抽象类的)Mock测试-模拟测试。
下表是一个对三种流行C++单元测试框架的简单比较,Gtest虽然发展起来的较晚,但丰富功能简单易用,易学,加之移植性较好,是跨平台项目单元测试框架比较好的选择。
表1.单元测试框架比较
测试框架支持特性
Gtest
BoostTest
CPPUnit
可移植性
较好
好(依赖于Boost库)
较好
丰富的断言
优
优
一般
丰富的断言信息
优
良好
较差
自动检测和注册测试用例
优
良
一般
易于扩展断言
易于扩展
一般
一般
支持死亡和退出测试(Death和Exit)
支持
支持
不支持
支持参数化测试(Parameterizedtest)
支持
支持
不支持
支持Scoped_Trace
支持
不支持
不支持
支持选择性执行测试用例
支持
支持
支持
丰富的测试报告形式(xml)
支持
支持
支持
支持测试用例分组Suites
支持
支持
支持
开源
是
是
是
执行速度
快
快
快
基于接口的Mock测试
通过Gmock支持
不支持
不支持
易用性
优秀
较复杂
较好
支持类型化的参数化测试
支持
不直接支持
不直接支持
回页首
测试驱动开发-GTest简介
Gtest是基于xUnit的C++单元测试框架,支持自动化案例自动发掘,丰富的断言功能,支持用户自定义断言,支持死亡测试和退出测试,还有异常测试控制,支持值类型和类型化的参数化测试,接口简单易用,对每个测试案例有执行时间的输出,可以帮助分析代码的执行效率,单一接口文件gtest.h。
图1是Console模式输出用红和绿表示失败和成功的测试用例,看起来比较符合TDD的策略和定义
图1.GTest的案例测试结果输出
Gtest的断言有两种形式,致命性断言(FatalAssertion)和非致命性断言(NonfatalAssertion)。
除了基本的断言形式外,Gtest还包括一些其他的高级断言形式,比如死亡断言,退出断言测试和异常断言等。
Gtest还有其他的一些特性,比如类型参数化测试,值类型参数化的测试,测试用例分组,洗牌式测试等,可以参照附录中列出的Gtest的官网获取更多的信息。
在测试驱动软件开发的过程中,我们不可避免的要去依赖第三方系统,比如文件系统、第三方库、数据库访问,其他的在线数据的访问等,按照测试驱动开发的快速反馈的原则,如果在单元测试用例中去直接访问这些信息,势必在测试驱动开发过程中会依赖这些资源从而造成访问时间无法控制,所以单元测试一般应该避免直接访问第三方系统,这就是Mock测试的主要目的,用模拟的接口去替换真实的接口,模拟出单元测试需要的第三方数据和接口进而隔离第三方的影响,专注于自己的逻辑实现。
Gmock就是这样一个Mock框架,它是类似于jMock、EasyMock和Hamcres,但是是C++版本的Mock框架。
Gmock是基于接口的Mock框架,在C++中接口的定义是通过抽象函数和抽象类来实现的,这种要求势必会要求我们尽量遵循基于接口的编程原则,把交互界面上的操作抽象成接口,以便是接口可被模拟Mock。
可以在附录中列出的Gmock官网获取更多信息。
回页首
测试驱动开发的实践
测试驱动开发和敏捷开发是相辅相成的,敏捷开发的需求一般是以故事、产品功能列表,或需求用例的方式给出,拿到这些需求后,开发团队会根据相应的需求文档分析需求,做功能分解,根据功能优先级制定迭代开发计划和测试计划。
测试驱动开发可以从两个角度来看,广义的和狭义的。
广义的测试驱动开发是从流程上规定测试驱动开发,这种情况下一般要求QA走到前面,先根据需求先开发测试用例,这些测试用例会作为功能验收的标准,然后开发人员会根据测试用例做详细的功能设计和编码实现,最后提交给QA做功能验收测试。
狭义的测试驱动开发是开发人员拿到功能需求后,先自己开发代码级别的测试用例,然后开发具体的实现通过这些测试用例的一种开发方法。
本文涉及的是第二种,从代码级别开始的,狭义的测试驱动开发。
相信每个人都玩过棋牌游戏,简单起见,为了实践测试驱动开发方法我想开发一款简单的三子棋游戏,如图2所示。
三子棋的游戏规则很简单,只要是同样的三个棋子连成一条线那么持对应棋子的人就胜出,图中持O子棋的人获胜。
总结一下三子棋游戏的基本需求:
1.我需要一个3X3的棋盘,可以用下三子棋。
2.我需要在棋盘上下棋和获取到棋子。
3.我要能验证和判断是不是三个棋子在同一条线上,以判断是不是有人胜出。
4.我不能放棋子到已被占用的棋位置上。
5.我要能判断是不是棋盘已满并无赢家。
6.我需要能复位棋盘,以便于重新开始下棋。
7.我需要用对记住玩家,以便于我能特例化Player
8.我需要能保存和加载棋局能力,以便于我能下次回来继续之前的游戏。
图2.三子棋游戏
以上是三子棋游戏的基本需求列表,拿到这些需求后,我会做一些简单解决方案的设计,解决方案包括4个子工程(C++Project),其中一个测试工程TicTacToeGamingTest,其余三个分别是TicTacToeLib,TicToeGamingLib和TicTacToeConsoleGaming,这三个工程的依赖关系是TicTacToeConsoleGaming依赖于TicToeGaminglib和TicTacToeLib,TicToeGamingLib依赖于TicTacToeLib。
建好这些工程,有了基本的设计思路后,在测试工程里首先开发的测试代码。
图3.解决方法设计
先看第一个需求:
1.我需要一个3X3的棋盘,可以来下三子棋。
这个需求很简单,现在的棋盘不需要包括任何的逻辑,为了便于测试我需要一个接口去访问它,现在接口是空的,也没有实现,这样一个测试用例就可以满足这个需求:
TEST_F(TicTacToeTestFixture,IWantAGameBoard)
{
IGameBoard*gameBoard=NULL;
EXPECT_NO_THROW(gameBoard=newSimpleGameBoard("simpleGame"));
EXPECT_TRUE(gameBoard!
=NULL);
EXPECT_NO_THROW(deletegameBoard);
}
这是第一个测试用例,稍微解释一下。
TicTacToeTestFixture是用于测试的分组的,它是一个类,继承于Gtest的test类testing:
:
Test,这个类可以重载setup和teardown等虚拟函数用于测试准备和清理测试现场。
TEST_F 是定义测试用例的宏,IWantAGameBoard是测试的案例的名称,会显示在输出中,测试用例很简单,只是只是保证能创建和析构SimpleGameBoard实例,并无异常抛出。
这个测试用例现在是不能编译通过的,因为IGameBoard接口和SimplegameBoard都还没有声明和定义,接下来为了使这个案例通过,我在TicTacToeLib工程里,声明和定义IGameBoard和SimpleGameBoard类,IGameBoard是纯抽象类,抽象了所有对棋盘的操作。
引入声明到测试工程中,编译通过并运行,现在完成了第一测试用例,尽管测试的IGameBoard和SimpleGameBoard还是空的。
可以看一下输出:
图4.测试用例输出
2.我需要在棋盘上下棋和获取到棋子
这个需求能使棋手在棋盘上把棋子放到想要的位置上并能查看指定棋盘位置上的棋子,棋盘是3x3。
实现这个需求也很简单,我只要在IGameBoard接口上添加两个函数然后在SimpleGameBoard里实现这两个函数就可以满足这个需求:
virtualvoidPutChess(intx,inty,charchess)=0;
virtualcharGetChess(intx,inty)=0;
有了这个思路,我想这样设计这个测试用例:
TEST_F(TicTacToeTestFixture,PutandGetChess)
{
charxChess='X';
charyChess='Y';
IGameBoard*gameBoard=newSimpleGameBoard("simpleBoard");
gameBoard->PutChess(0,0,xChess);
gameBoard->PutChess(2,2,yChess);
EXPECT_EQ(xChess,gameBoard->GetChess(0,0));
EXPECT_EQ(yChess,gameBoard->GetChess(2,2));
deletegameBoard;
}
试着编译这个测试工程,失败,原因是没有实现这两个函数,接下来我回到TicTacToeLib工程去声明和定义这两个函数。
为了实现这两个功能,在SimpleGameBoard定义private数据:
vector
两个函数的实现是:
voidSimpleGameBoard:
:
PutChess(intx,inty,charchess)
{
assert(x intxy=x*3+y; if(data_.size()==0)initboard_(); data_[xy]=chess; } charSimpleGameBoard: : GetChess(intx,inty) { assert(x assert(data_.size()==yMaxDim*xMaxDim); returndata_[x*3+y]; } initboard_()是个protected函数,用于初始化data_。 现在可以重现编译和运行测试工程,结果如下: 图5.测试用例输出 有了两个测试用例的实现,并且运行是绿色,继续下个需求。 3.我要能验证和判断是不是三个棋子在同一条线上,以判断是不是有人胜出 这个需求用于判断三个棋子是否已经在一条线上,如果是的话,那么持对应棋子的棋手就会胜出,这个测试用例可以这样设计: TEST_F(TicTacToeTestFixture,JugeThreeInLine) { IGameBoard*gameBoard=newSimpleGameBoard("simpleBoard"); IGameBoard*gameBoard2=newSimpleGameBoard("simpleboard2"); charxChess='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)); deletegameBoard; deletegameBoard2; } 设计是这样的,为简单,我把判断棋子胜出的函数CheckWinOut定义到接口IGameBoard中,并在SimpleGameBoard中实现它,实现如下: boolSimpleGameBoard: : CheckWinOut(charchess) { returnIsThreeInLine_(chess); } IsThreeInLine_是受保护的成员函数,它会扫描棋盘的行,列和对角线看是否指定的棋子在一条线上,如果有三个棋子在一条线上,则说明有人胜出。 编译运行测试,绿色通过。 继续下一个需求。 4.我不能放棋子到已被占用的棋位置上。 这个需求是个验证性需求,要保证棋子不能重叠和覆盖已在棋盘上的棋子,实现这个需求我只要重构现有的代码加上避免棋子重叠的逻辑。 只要避免在PutChess时候,检查是否指定的位置是否已有棋子,如果是简单的抛出异常即可。 有了这些基本的思路,我开始设计测试用例。 TEST_F(TicTacToeTestFixture,BizException_Occupied){ IGameBoard*gameBoard=newSimpleGameBoard("simpleboard"); charxChar='X',yChar='0'; EXPECT_NO_THROW(gameBoard->PutChess(0,0,xChar)); EXPECT_THROW(gameBoard->PutChess(0,0,xChar),ChessOverlapException); EXPECT_NO_THROW(gameBoard->PutChess(2,2,yChar)); EXPECT_THROW(gameBoard->PutChess(2,2,yChar),ChessOverlapException); deletegameBoard; } ChessOverlapException是我将要实现的一个异常类,这个是在棋手试图放棋子到已有棋子的棋盘位置上时要抛出的异常。 测试用例中,我在(0,0)和(2,2)这两个位置上放同样的棋子以触发这个异常。 为了编译通过,我开始实现ChessOverlapException。 ChessOverlapException继承自std: : exception我重载了what函数返回相应的异常信息。 把这个异常类的定义引入的测试工程中,编译通过运行测试,但却得到了红色Red,案例失败: 图6.测试用例输出 点击查看大图 原因是我还没有重构PutChess函数以加入避免棋子被被覆盖的代码。 现在来重构PutChess函数: voidSimpleGameBoard: : PutChess(intx,inty,charchess) { assert(x intxy=x*3+y; if(data_.size()==0){ initboard_(); data_[xy]=chess; return; } if(data_[xy]! ='+'){ throwChessOverlapException("chessoverlap! "); } elsedata_[xy]=chess; } 重新编译测试工程并运行得到绿色Green通过。 继续下一个需求。 5.我要能判断是不是棋盘已满并无赢家。 这个需求用于判断是否是和棋的情况,棋盘满了但并无赢家,这是可能出现的一种情况,这个实现设计可以有两种方式.一是重构CheckWinOut函数,使返回值携带更多的信息,比如和棋,有人胜出等。 二是定义一个独立的函数去判断棋盘的当前状态。 第一种方案较合理,开始设计这种方案的测试用例: EST_F(TicTacToeTestFixture,IsEndedInADraw) { charxChess='X',yChess='O'; IGameBoard*gameBoard=newSimpleGameBoard("simpleBoard"); gameBoard->PutChess(0,0,yChess);gameBoard->PutChess(0,1,xChess);gameBoard->PutChess(0,2,yChess); gameBoard->PutChess(1,0,xChess);gameBoard->PutChess(1,1,yChess);gameBoard->PutChess(1,2,yChess); gameBoard->PutChess(2,0,xChess);gameBoard->PutChess(2,1,yChess);gameBoard->PutChess(2,2,xChess); GameBoardStatusstatus=gameBoard->CheckWinOut(yChess); EXPECT_TRUE(status==GAMEDRAW); GameBoardStatusstatus2=gameBoard->CheckWinOut(xChess);EXPECT_TRUE(status2==GAMEDRAW); deletegameBoard; } 以上的测试用例可以看出,我设计了和棋的棋局,并想重构CheckWinout函数,使其返回枚举类型GameBoardStatus以表示棋局的状态,其中GAMEDRAW表示和棋状态。 为了使工程能编译通过,开始定义这个枚举类型并重构CheckWinOut函数。 实现所有设计,经过几次的Red失败,最终形成代码: GameBoardStatusSimpleGameBoard: : CheckWinOut(char
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 面向 C+ 的测试驱动开发 测试 驱动 开发