财新传媒
位置:博客 > 万战勇 > 【老万】C++测试框架为啥星星一样多?

【老万】C++测试框架为啥星星一样多?

 

(本文首先用 E 文于2012年10月发表在 Google Testing Blog。)
 
这年头哇,好像每个 C++ 程序员都在写C++测试框架呢——要是他还没写过的话,大概率就是在去写的路上。
 
要是不信,你到维基百科(https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#C.2B.2B)了解一下。天呐,这个名单望不到边啊!
 
有意思。我说伙计,要知道,很多面向对象语言只有一两个流行的测试框架啊。比方说,Java 程序员爱用 JUnit 或者 TestNG。莫非 C++ 程序员生来就爱 DIY 不成?
 
俺开始写谷歌 C++ 测试框架(Google Test)的时候,还有把它开源的时候,好多人问俺弄啥咧!长话短说,要是有现成的便宜俺还能不拣吗?就是因为找啊找啊找不到一款能用的框架,没办法才自己上的啊。
 
俺不是针对任何人啊——俺可没说很多现成的框架是辣鸡。事实是它们很多不是辣鸡!不但不是辣鸡,还有不少有真知啊灼见啊啥的,俺都掏小本本学习了呢!
 
可你知道不,谷歌的 C++ 项目那叫一个多,啥操作系统上的都有啊(像 Linux,Windows,Mac OS X,安卓,塞班啦,等等等等),还有各式各样的编译器!不同的项目吧,编译器开关还设得不一样咧!俺们要的是一款能适应各种环境的万能框架:无论何时,无论何地,心中一样,亲!
 
Java 尝曰:“写一次,跑四方(Write once, run everywhere)”。俺们 C++ 程序员可木有那个福分呐!俺们可是啥环境都得防着!你看,C++ 语言本身已经够复杂了吧,还尽被用来干底层的脏活儿。所以,不同的编译器之间不好兼容咧!就算是同一个牌子的编译器,3.6 版跟 3.5 都不一样咧!
 
这位看官问了:不是有 C++ 标准吗?
 
憋提了!说是“嫑准”还差不多。这几年还算好的了,前些年啊,那些个写编译器的都不尿这个标准咧!愣搞出些个张家港 C++ 2.0,马王堆 C++ 2.7,公主坟 C++ 3.0 啥的,互相不对付啊!他们这一搞,弄得好多任务不上非标准的扩展功能搞不定咧!你看看,用 C++ 写个到处能使的复杂玩意儿忒难咧!
 
这还不算,好些个 C++ 编译器还嫌世道不够乱,居然让人可以选择关掉一些标准的语言功能呢!图个啥啊?还不是为了从 CPU 里挤榨出更多的性能啊。
 
不想要异常(exceptions)吗?关了它可以节省 1%-2% 的资源!不待见动态类型转换?咱把它的根基 RTTI (Run-Time Type Identification)都能拔了就问你服不服!
 
不过嘛,这么一搞就成了东方不败版 C++ 了,好些个代码可就编译不过了啊,嘿嘿!
 
老夫夜观天象,有忒多测试框架是基于异常实现的,俺们只好跟它们就此别过——谁让谷歌贼多项目都关闭了异常涅。
 
自夸一下:俺歌测试框架不挑待遇,不要异常和 RTTI 也能工作;要是运气好碰上个支持异常和 RTTI 的环境呢,Google Test 还会加点料,提供一些附加功能,比如用来测试异常的断言 EXPECT_THROW 啥的。
 
看官又要问了:写个移植性好的框架不就结了?为毛大家前赴后继地添框加架涅?
 
你看你看,这不就是 Google Test 要解决的问题吗。其实啊,俺们也不是头一个想这么搞的。其他人也试过咧。不过嘛,你要知道,这么搞是有代价滴,好多人没弄成咧。
 
跨平台的 C++ 系统写起来费忒多劲了:你得保证它在不同的操作系统、不同的编译器品牌/版本、不同的编译开关环境下都能正常工作。再考虑到这些因素的排列组合,乖乖,俺想闪咧!要是碰上个缺德的平台不让你干这干那的,还得找个备胎方案不是。
 
还有哇,编译器也会有 bugs 咧,不同版本的编译器 bugs 还不一样咧。为了世界和平,俺只好扭曲代码把各种 bugs 都绕开,苦啊!
 
所以说,除非你是为了评职称搞一个样子货,真要把这个东西弄得普适了,难呐!
 
综上所述,俺以为 C++ 测试框架多如繁星的重要原因是 C++ 不是一个人——一千个人就有一千个哈姆雷特,一万个环境就是一万个 C++ 啊。想写一款啥地儿都能跑的框架不容易,写出来你就离垠神只差二级了。
 
还有啊,C++ 自身先天不足,有些功能不好写咧。比如说,大多数 Java 测试框架都通过反射(reflection)来发现测试案例,这样可以省了手动注册案例的麻烦。然而!C++ 是不支持反射的。大家只好土法上马,想出各种奇葩招数来弥补 C++ 的不足。
 
遗憾的是,各花入各眼,各马配各鞍,每个 C++ 测试框架的作者都有自己的偏好,谁怕谁啊!这就进一步造成了 C++ 测试框架阵营的有丝分裂:有的框架就破罐子破摔让你手工注册了,有的高大一点用脚本扫描源代码来发现测试案例,还有的用宏(macros)来实现自动注册。俺觉得宏不错,老少咸宜,但也有人不同意啊。再说了,即便是大家都选择用宏,还有不少实现方法可以选择咧!它们各有所长,没谁能一统江湖。
 
咱来看看 Google Test 咋注册测试案例的吧。要加一个案例,最简单的办法是用 TEST 这个宏:
TEST(Foo, HasBarProperty) {
  … 测试代码往这儿放 …
}
这个例子里,俺们定义了一个名字叫 Foo.HasBarProperty 的测试案例。顾名思义,它的目的呢,就是验证 Foo 这个东西有 Bar 这个特性。这个 TEST 宏会自动告诉框架:俺这儿有个案例啊,叫这个名字,回头运行的时候憋忘了叫俺啊!
 
再看一个具体案例:验证阶乘(Factorial)函数可以处理正的输入:
TEST(Factorial, HandlesPositiveInput) {
  EXPECT_EQ(Factorial(1), 1);
  EXPECT_EQ(Factorial(2), 2);
  EXPECT_EQ(Factorial(3), 6);
  EXPECT_EQ(Factorial(8), 40320);
}
 
还有一点啊,忒多写 C++ 测试框架的人没把可扩展性当回事咧。他们看到一个问题,解决一个问题,再看到一个问题,再解决一个问题...... 殊不知问题是子子孙孙无穷尽的。结果呢,俺们得到了一堆解决方案,每个用来解决某一类特定问题都能使,但是都不够通用。这好比方向盘固定的车,出门只能对准了直行,左转就歇菜了。
 
欲练神功,必先......支持用户扩展。俺们得面对现实:即便 Tony Stark 再世也不是万能滴。与其在框架里塞满八百年也用不到的东东,不如只提供满足 95% 需求的功能,把剩下的留给用户去展现他们的才艺——前提是你得提供这个舞台。
 
想想也是咧,要是有人搭台俺只需要贴几张墙纸,谁有病非得从泥水工做起不成啊?可惜啊,太多太多的框架作者只图眼前爽,忽视了可扩展性。俺以为,这种心态是造成今天 C++ 测试框架泛滥的重要原因。
 
俺们写 Google Test 的时候,可是把可扩展性当成圣旨咧。比如,系统内置的测试断言不够用咋办?必须有办法让同学们轻松定义新的测试断言,而且这些用户定义的断言必须和系统内置的一样好用。
 
举个例子啊:某天,程序员二柱子要测试某个整型函数的返回值在某个区间内。请看他的处女作:
bool IsInRange(
    int value, int low, int high) {
  return low <= value &&
         value <= high;
}
  ...
  EXPECT_TRUE(IsInRange(
      SomeFunction(), low, high));
这个,不是说它不行,问题是它产生的错误信息让人无语啊!
 
做为一个测试宏,EXPECT_TRUE 只能看到表达式 IsInRange(SomeFunction(), low, high) 的结果(true 或者 false)。它也不知道 SomeFunction() 的值有多大,它也没法问 low 有多 low,high 有多 high。所以从错误信息中俺们只知道结果不对,却不知道到底哪里不对。
 
写出这样没用的测试来,二柱子今年升级无望啊!
 
好吧,俺改:
  EXPECT_TRUE(IsInRange(
      SomeFunction(), low, high))
      << "SomeFunction() = "
      << SomeFunction() 
      << ", not in range ["
      << low << ", " << high << "]";
 
这次要是测试失败了,EXPECT_TRUE 会打印一些附加信息帮助 debug,OK 不?
 
不,这不 OK !侬看,要是这 SomeFunction() 每次返回的数不一样咋办?比如,要是 IsInRange(...) 里的那个 SomeFunction() 返回 42,<< 后面的 SomeFunction() 返回 250,你欲哭有泪没?
 
看来俺得祭出小学数学大法之提取公因式啊:
  const int result = SomeFunction();
  EXPECT_TRUE(
      IsInRange(result, low, high))
      << "SomeFunction() = " << result
      << ", not in range ["
      << low << ", " << high << "]";
 
这...... 行倒是行了,可您不觉着麻烦吗?要是这个套路需要重复 20 遍,您还真这么硬刚啊?
 
No!俺们一定要找到写测试的金光大道!
 
听人讲,搞计算机最重要的三个要素就是:抽象、抽象、抽象!俺们就是要把这个套路抽象成一个可以重用的玩意儿。
 
在 Google Test 里,你可以这样来解决这个套路问题:
AssertionResult IsInRange(
    int value, int low, int high) {
  if (value < low)
    return AssertionFailure()
        << value << " < lower bound "
        << low;
  if (value > high)
    return AssertionFailure()
        << value << " > upper bound "
        << high;
  return AssertionSuccess()
      << value << " is in range [" 
      << low << ", " << high << "]";
}
这个函数定义好了,就可以反复使用了:
  EXPECT_TRUE(IsInRange(
      SomeFunction(), low, high));
要是这个断言失败了,错误信息是这样的:
Value of: IsInRange(
            SomeFunction(), low, high)
  Actual: false (13 < lower bound 20)
Expected: true
要是想反过来用也可以:
  EXPECT_FALSE(IsInRange(
      AnotherFunction(), low, high));
失败的话画风是这样的:
Value of: IsInRange(
            AnotherFunction(), low, high)
  Actual: true (25 is in range [20, 60])
Expected: false
这样,慢慢儿地二柱子就可以积累出一个针对自己项目的测试断言库啦。用了这个库,代码整洁了,错误清晰了,bugs 不好躲了,二柱子升职就快了。
 
按同样的思想指导,Google Mock(谷歌的 C++ mocking 框架)允许用户自定义匹配器(matchers)来扩充系统的匹配器词汇量——这个比上面 IsInRange() 的例子还要方便灵活些。具体情况咱们回头聊啊。Google Test 还有一个 event listener API 可以用来写插件(plug-ins),比如把测试结果写成 XML 报告或者在图形界面显示出来。
 
俺希望,人类善加利用这些功能,不断扩充 Google Test/Mock 的词汇库来解决他们的日常问题,再把这些扩充功能回馈到开源社区。你帮俺,俺帮你,世界更美好,吼吼!
 



推荐 1