为什么有如此多的C++测试框架 - from Google Testing Blog

Why Are There So Many C++ Testing Frameworks? by Zhanyong Wan (Software Engineer)

 

最近貌似有很多人正在开发他们自己的C++测试框架,如果还没能完工。Wiki上有一个此类框架的不完全列表。因为大多数面向对象编程语言只有1到2个主要的框架,对C++而言这就显得有趣了。例如,大多数Java开发者都使用JUnit或TestNG。难道C++程序员都是疯狂的DIY爱好者吗?

当我们开发并将Google Test(Google的C++测试框架)开源后,人们开始好奇为什么我们要做这件事。简单的回答是我们没有能够找到一个已有的并且能够满足需求的C++测试框架。这并不表明现有的框架设计和实现的很糟糕,事实上我们也从中学到了很多伟大的想法和实现技巧。但是Google有如此多的C++项目需要在不同的平台上用不同的编译器编译,同时还有着无数的编译选项,我们确实需要有这么一个框架能够在如此复杂环境下处理不同类型和规模的项目。

和有着著名口号“一次编程,到处运行”的Java不同,C++代码的编写在一个更复杂多变的环境下进行。由于语言本身的复杂性以及处理底层任务的需求,不同的C++编译器甚至不同版本之间的兼容性都存在着许多问题。虽然有着C++标准,但是到现在为止还没有一个编译器厂商能很好的全面支持。很多时候因为任务需要,你必须使用一些不可移植的扩展或者平台相关的功能。以上这些原因使得编写一个能使用不同编译器在在不同平台上工作的复杂系统变得十分困难。

更头大的事还没完,大多数C++编译器允许你关闭一些标准语言特性来获取更好的性能。不喜欢异常?关了。不喜欢动态转换Dynamic Cast?把动态类型识别RTTI禁用好了,虽然它能提供动态转换和运行时类型信息访问。如果你选择这么做,那么使用到以上特性的代码就会编译失败。许多测试框架都依赖异常,想想你把异常关掉会之后发生什么?但对于我们来说这不是一个问题,因为我们的大多数项目都是禁用异常的。也许你会好奇,但是让我告诉你,Google Test在默认情况下是不需要异常和动态类型识别的。但是当这些特性被打开时,Google Test会试着去利用它们给你提供一些新功能,比如基于异常的断言exception assertions。

为什么不写一个可移植的框架?是的,这是Google Test的最高设计目标,而且很多框架的设计者也已经努力过了。但是,自由不是无代价的。跨平台的C++开发要求大量额外的精力:

  1. 你必须使用不同的编译器,摆弄不同的编译开关,生成代码在不同的操作系统上运行测试。
  2. 某些平台的特殊要求使得你必须使用条件编译。
  3. 不同的编译器可能存在bug,你必须小心的修改代码绕来绕去。
  4. 除非你在一台裸机上工作,不然这一切太让人崩溃了。

所以,我的结论是:为什么我们有如此多C++测试框架的原因是出在C++实现自身,不同环境之间的变化使得编写可移植的C++代码是如此困难。张三的框架可能完美的解决了张三的问题,但是对李四却一点不适用。

另外一个原因我想是C++自身的限制使得我们无法很好的实现一些功能,为了绕过一些限制大家只好各显神通了。一个显著的例子就是C++是一个静态类型语言并且不提供反射Reflection机制。大多数Java测试框架可以使用反射自动找到你编写的测试用例,这样你就不用一个个去手工注册了。如果需要手动注册的话很有可能你写了测试用例而忘记去注册,并且手工维持更新也是很痛苦的。因为C++没有提供反射机制,我们只能用不同的方法。不幸的是没有一个完美的解决方案。一些框架要求你手工注册测试用例,而另一些框架使用脚本分析你的代码来找到测试用例,还有一些则利用宏来完成自动注册。我们认为最后一个使用宏的解决方案是最好的,并且对大多数人都有效。目前有好几种用宏来实现的方法,各有各的利弊,最终结论还是有待商榷的。

让我们看一些实际代码来理解Google Test是如何解决测试用例注册问题的。最简单的方法是使用TEST宏来添加一个测试用例。

1 TEST(Subject, HasCertainProperty) {
2     // … testing code goes here …
3 }

它定义了一个测试用例来检查某个Subject是否包含一些特定的属性。宏自动向Google Test注册这个测试用例,所以当测试程序执行时这个用例会被覆盖。

这里有个更加实际的用来检测阶乘函数在使用正整数时能正确工作的例子:

1 TEST(FactorialTest, HandlesPositiveInput) {
2     EXPECT_EQ(1, Factorial(1));
3     EXPECT_EQ(2, Factorial(2));
4     EXPECT_EQ(6, Factorial(3));
5     EXPECT_EQ(40320, Factorial(8));
6 }

最后,许多C++测试框架的作者都忽略了扩展性并且满足于提供一个打包了的解决方案,这导致的后果就是我们留下了很多解决方案,每个都只针对一些特定的问题而不足够通用。一个万能的框架必须提供足够的扩展能力。我们必须明确无论如何所有人的需求是不可能被同时满足的。我们可以提供一个一揽子解决方案满足95%的需求,而不是为了一些很少有人用的功能而塞入大量臃肿的代码。我们可以开放接口来让用户自己根据需要实现那些功能。如果我能轻松的扩展已有框架来实现我需要的特定功能,我就不会选择重起炉灶写一个新的框架。但是大多数框架的作者都没有看到可扩展性的重要。我认为正是这种思路导致了今天这种群魔乱舞的局面。在Google Test的实现中,我们努力使得你可以通过自定义断言生成更有意义的错误信息来简单的扩展你的测试词汇表。这里有个简单的例子显示如何判断一个值是否在给定的范围:

1 bool IsInRange(int value, int low, int high) {
2     return low <= value && value <= high;
3 }
4  
5 // ...
6  
7 EXPECT_TRUE(IsInRange(SomeFunction(), low, high));

当你的断言失败时,你只知道函数SomeFunction返回的值不在[low, high]的范围中,但是你不知道返回值和期望范围是多少,这就使得代码的调试变得比较麻烦了。

但是你可以提供自己定制的信息来提供断言失败时更有意义的描述:

1 EXPECT_TRUE(IsInRange(SomeFunction(), low, high))
2     << "SomeFunction() = " << SomeFunction() 
3     << ", not in range ["
4     << low << ", " << high << "]";

如果考虑到SomeFunction可能每次返回不同的值,我们可以把代码稍作修改:

1 int result = SomeFunction();
2 EXPECT_TRUE(IsInRange(result, low, high))
3     << "result (return value of SomeFunction()) = " << result
4     << ", not in range [" << low << ", " << high << "]";

上面的办法看着可行但是太麻烦了,假如你要调用几百次EXPECT_TRUE来检测SomeFunction的返回值怎么办?我们需要把这种模式抽象为一个可重用的结构。

Google Test允许你自定义类似于如下的测试断言:

 1 AssertionResult IsInRange(int value, int low, int high) {
 2     if (value < low)
 3         return AssertionFailure()
 4             << value << " < lower bound " << low;
 5     else if (value > high)
 6         return AssertionFailure()
 7             << value << " > upper bound " << high;
 8     else
 9         return AssertionSuccess()
10             << value << " is in range [" 
11             << low << ", " << high << "]";
12 }

当范围是[20, 60]而SomeFunction返回13时如下信息将被打印:

Value of: IsInRange(SomeFunction(), low, high)
  Actual: false (13 < lower bound 20)
Expected: true

同样的IsInRange定义还可以被用于EXPECT_FALSE的情况,例如"EXPECT_FALSE(AnothFunction(), 20, 60)",AnotherFunction返回25,如下信息将被打印:

Value of: IsInRange(AnotherFunction(), low, high)
  Actual: true (25 is in range [20, 60])
Expected: false

通过以上方式,你可以为特定的问题领域建立一个断言库,从这些清晰的,陈述性的代码和有意义的错误信息中获益。

于此一脉相承的还有Google Mock(我们的C++ mocking框架)允许你自定义matchers和系统自带的matchers一起无差别的使用。我们还在Google Test引入了事件监听API使得用户可以自行编写插件。我们希望用户能够充分使用这些特性扩展Google Test和Mock以满足他们的需求,如果能提交一些好的扩展给我们那是最好不过的了。

就像共产主义一定要实现一样,总有一天,C++测试框架碎片化的问题也会得到解决。

posted @ 2013-10-11 18:21  移山测试工作室黑灯老师  阅读(2252)  评论(0编辑  收藏  举报
count website visits
Buy Computers