单元测试长期以来一直被打入软件开发的冷宫,究其原因,大概是意识形态的问题。如果询问开发人员:为什么不采用单元测试?得到的回答十有八九是这种模式:写代码时间都不够更何况去做测试了。(我原先也是属于那八九中的一个)但是,有一个结论:写单元测试可以节省开发时间。这也许令人匪夷所思,但是,经过切身体会,确实如此,不过有一个前提:在编码前写测试。很有些反其道而行之的韵味。当年在侠客岛上,众多中原顶尖侠客参悟“侠客行”无果,而不识字的少年却窥探到武学精髓。很多事情往往是这样的,按常理行不通,换个方向则行云流水。且说要编写的代码,就好比少林寺的武僧,在下山行侠之前要经过少林铜人阵的考验。众多武僧在铜人阵中因为一招稀奇古怪的招式败下阵来比比皆是。如何才能通过铜人阵的考验呢?如果从修行的开始就了解铜人阵,并在修行中不断的接受铜人们的折磨,那么最后的通过将不成问题。本着以上的指导思想,在编写代码前先设计测试用例,之后再编写代码,这么做的好处是:从一开始就明白代码的目的是什么,对于代码将会受到的折磨了然于胸,用术语说就是:需求明确,代码覆盖率高。我的切身体会是在使用这种方法后犯低级错误的概率减小了,几乎不会使用单步测试。现在算是对Kent Beck大师所描述的那种十分钟不做单元测试都会觉得心惊肉跳的心情有所体会了。还有一个好处,就是复用程度的提高。这个好处的论证有点诡辩的味道,首先,编写出来的代码本身就有一个用户,那就是我们需要实现的软件中的代码,另外一个用户则来自于测试用例,因此,这样的代码本身的重用性就较强。这点也不得不承认,在编写代测试用例的时候,将会不自觉地考虑到在软件中的嵌入,因此,这种情况下作出的设计会有较低的耦合性。将测试驱动与经典的先代码后测试进行比较,很重要的一个将是面临的心境的不同。写好的代码是自己的血肉,没有人会希望它脆弱,也不愿意看到它的失败,因此,在测试的时候,往往担惊受怕,手指在回车键上方盘旋,绷紧的神经频频接受考验。而对于测试驱动来说,出生于艰苦的环境,每天努力打拼,为的就是能够出人头地,成长为健壮的代码,因此我们会毫不犹豫的按下回车键,让暴风雨来得更猛烈些吧!
在进行测试驱动的过程中,有一点是很重要的,那就是保持它的旋律。好比一出精彩的电影,有黯然神伤的低谷,也有激情澎湃的高潮。测试驱动开发的旋律就是:设计测试用例,编写最少的代码通过测试,重构代码,设计下一个测试用例。其中,在编写代码通过测试及重构代码的过程往往需要几个迭代过程,期间跌宕起伏,精彩刺激。在设计好测试用例后,对其进行编译,这时将会遭遇到第一个低谷:编译错误。我们的努力目标是通过编译,这时候,我们编写最少的代码通过编译,这样迎来了第一个上升期,但是又将面临则测试无法通过的问题,于是再接再厉,增加代码的功能,这样,我们通过了测试,也迎来了第一个高潮。美好的事情总是需要变得更美好,于是我们着手对代码进行修整,使它更优雅,我们不允许任何的重复,copy&paste是我们的死敌,不允许看不懂的代码,那是理解的障碍,不允许任何不新鲜的味道从代码中飘出。重构将帮助我们达到尽善尽美,而这一切的代价仅仅是几分钟的时间。但是,不要操之过急,否则会烫坏舌头的。一步一步的进行,每次的修改都要保证之前通过的测试,这样,我们最终将稳步的达到幸福的彼岸!
人都是有惰性的,惰性常常与失败,错误联系在一起。但是,惰性也代来了很多的好处。又是一个反其道而行之的例子。在使用测试驱动的过程中,如果每次进行测试都需要组织测试的环境及输出格式,并且比较屏幕上飞出来的数据,那么,再勤劳的人也无法坚持这项工作,测试驱动也将成为一个美丽的空中楼阁。要充分发挥测试驱动的威力,必须具备以下“必要非充分”条件:能够方便建立测试环境;有良好的输出形式;可以方便的比较测试结果。有了以上条件后,人们可以在弹指间建立测试环境,进行频繁的测试,让自己的代码在出山前接受频繁的考验。JUnit是Kent Beck大师建立起的一套JAVA环境下的单元测试框架。它能够满足上述测试驱动的必要条件,提供了方便的测试环境,良好的屏幕输出以及结果比对机制。虽然JUnit是JAVA的,但是测试驱动却是所有开发者的。对于行走于CPP世界的人们,CPPUnit可以满足他们的要求。 CPPUnit是Michael Feathers建立一个开放源码的单元测试库,是JUnit的C++版本,同样提供了便利的条件。它的老巢位于http://sourceforge.net/projects/cppunit/。在将CPPUnit应用于测试驱动开发之前,首先要明了几个概念:CPPUnit按照层次管理测试。最底层的就是Test Case,这里是测试代码存在的地方,换句话说,就是测试函数。当有了几个Test Case以后,可以把他们组织成Test Fixture。在Test Fixture中,可以建立被测试的类的实例,并编写Test Case对类实例进行测试。当有了多个Test Fixture后就可以使用Test Suite来对测试进行管理。借用CPPUnit上的例子,需要设计一个复数类,首先希望复数类能够使用“==”进行判断,因此,先构思一个Test Case:
class ComplexNumberTest : public CppUnit::TestCase {
public:
ComplexNumberTest( std::string name ) : CppUnit::TestCase( name ) {}
void runTest() {
CPPUNIT_ASSERT( MyComplex (10, 1) == MyComplex (10, 1) ); // note:1
CPPUNIT_ASSERT( !(MyComplex (1, 1) == MyComplex (2, 2)) ); //note:2
}
};
这个测试用例的意图很明显,就是对MyComplex类进行相等测试,根据复数的数学知识,能够得到note:1处我们期望的是相等,而note:2处则是不等。有了这个测试用例后,对其进行编译,由于MyComplex类没有进行定义,因此将无法通过编译,不过,这是单元测试中必然会遇到的问题。对于无法编译的恐惧驱使我们尽快的完成下面的代码:
//If we compile now ,we get compile error.So keep fixing it.
bool operator==( const MyComplex &a, const MyComplex &b)
{
return true;
}
//Now compile it again,Ok!Run it,we'll get some fail.
//This is because the operator==() doesn't work properly.Keep fixing it.
现在编译没问题了,可以松一口气了,不过无法通过测试,于是再接再厉,写下:
class MyComplex {
friend bool operator ==(const MyComplex& a, const MyComplex& b);
double real, imaginary;
public:
MyComplex( double r, double i = 0 )
: real(r)
, imaginary(i)
{
}
};
bool operator ==( const MyComplex &a, const MyComplex &b )
{
return a.real == b.real && a.imaginary == b.imaginary;
}
//If we compile now and run our test it will pass.
编译,测试,通过了,这个世界清静了,恍如来到了桃花源…
不过,我们要的尽善尽美,MyComplex不够美丽,于是改了个名字CComplex,这下大功告成,但是,心里始终还是有个结,如何把它用在自己的项目中呢?欲知后事,请代下回分解。
参考文献:
Wiki About TDD
http://c2.com/cgi/wiki?TestDrivenDevelopment
Unittests in extremeprogramming.com
http://www.extremeprogramming.org/rules/unittests.html
CPPUnit Wiki
http://cppunit.sourceforge.net/cgi-bin/moin.cgi/FrontPage
CPPUnit Cook Book
http://cppunit.sourceforge.net/doc/lastest/cppunit_cookbook.html
文章来源于领测软件测试网 https://www.ltesting.net/