Google C++测试框架系列:入门

原始链接:V1_6_Primer

  • GTest或者Google Test: Google的C++测试框架。
  • Test Fixtures: 这个词实在找不到对应的中文。
  • Bug: 太常用了,不翻译。
  • House keeping chores: 家常事务。指非核心的编码工作,比如测试代码的assert, log以及用例管理等工作。
  • set-up/tear-down: 指运行测试前的准备和之后的清理工作。
  • test case: 测试用例,管理测试的单位,一个测试用例可以包含一个或多个测试。

在阅读之前,推荐阅读《为什么有如此多的C++测试框架 - from Google Testing Blog》了解一些背景知识。

目录

 

介绍:为什么使用GTest?

GTest帮助你写更好的C++测试代码。

不管你在什么平台上工作,无论是Linux,Windows还是Mac,只要你使用C++,GTest就可以帮助你。

对于什么是一个好的测试,GTest如何来帮助实现这个目标,我们的观点如下:

  1. 测试必须是独立并且可重复的。如果某个测试的通过还是失败依赖于其它测试的执行结果,那么调试它将是非常困难的。GTest通过在不同的项目下分别执行测试使得它们相互隔离。当某个测试失败后,GTest允许你单独执行它以便快速调试。
  2. 测试的组织必须很好的反映测试代码的结构。GTest以测试用例为单位把那些能共享相同数据和代码的测试组织起来。这种常见的模式使得我们可以很方便的识别和维护测试。当人们切换项目在新的代码基础上工作时,这种一致性会显得特别有帮助。
  3. 测试必须可移植和可重用。开源社区的很多代码都是平台中立的,所以它们的测试也必须平台中立。GTest可以在不同的操作系统上工作,使用不同的编译器,支持或不支持异常,所以GTest可以很容易的在不同配置上工作。(注意:当前的发行版只包含Linux的构建脚本,我们正努力工作以支持其它平台。)
  4. 一个测试失败后必须提供尽可能多的提示信息。GTest不会因为一个失败就停止继续工作。它仅仅停止当前测试并且继续执行后续的测试。你还可以设置测试在遇到非致命失败时继续执行并打印相关信息。通过这种方法,你可以在一个“执行-编辑-编译”周期中发现和修复多处bug。
  5. 测试框架必须把测试编写者从繁重的家常事务中解脱出来以集中精力于测试本身。GTest自动维护和跟踪所有定义的测试,不需要你为了运行测试而去手工枚举所有测试。
  6. 测试必须迅速。使用GTest,你可以在不同测试之间使用共享的资源,而只需要做一次set-up/tear-down的工作,并且测试和测试之间相互独立。

因为GTest只用流行的xUnit架构,只要你熟练使用JUnit或PyUnit就一定会感到非常亲切。如果以前没玩过的话,只要花10分钟就可以上手了。好吧,我们继续!

开始一个新项目

使用GTest编写测试程序,你必须先把GTest编译成一个库文件然后在你的测试程序中链接它。我们为主流的构建系统准备了一些现成的构建脚本:GTest根目录下的msvc/用于Visual Studio,xcode/用于Mac的Xcode,make/用于GNU make,codegear/用于Borland C++ Builder,另外还有autotools script(已淘汰)和CMakeLists用于CMake(推荐的)。如果你的构建系统不在以上列表中,你可以参考make/Makefile文件学习GTest是如何被编译的(一般来说编译src/gtest-all.cc,把GTEST_ROOT和GTEST_ROOT/include加到头文件搜索路径中,GTEST_ROOT指GTest根目录)。

基本概念

使用GTest你肯定会接触到断言这个概念。断言是用来判断某个条件是否为真。一个断言的结果可以是成功,也可以是非致命失败或致命失败。如果发生了一个致命失败,当前函数就会退出,不然程序还是会继续正常执行。

测试使用断言来判断测试代码的行为。如果测试崩溃了或者断言失败,那么这个测试就失败了,不然就是通过。

一个测试用例包含一个或多个测试。你必须用测试用例把你的测试进行分组以反映测试代码的结构。当某个测试用例中的多个测试共享一些对象或程序时,你可以把这些对象和程序放进test fixture类。

一个测试程序可以包含多个测试用例。

我们现在开始讲解如何编写一个测试程序,先从单个断言开始,然后逐步到测试和测试用例。

断言

GTest的断言使用宏来组合一组函数调用。你通过对某个行为实施断言的结果来测试一个类或者函数。如果断言失败,GTest打印断言所在的文件和行数,以及失败信息。你还可以在GTest标准的输出信息之外添加自定义的失败信息。

对一个行为实施断言可能产生不同的影响。ASSERT_*版本在失败时会产生致命失败并退出当前函数。而EXPECT_*版本则产生一个非致命失败,而且不会退出当前函数。一般来说我们更倾向使用EXPECT_*版本的断言,因为这使得我们在一个测试中可以报告多个失败。但是如果失败后执行程序变得没有意义,那么你就该使用ASSERT_*版本。

因为ASSERT_*版本的断言在失败后立即从当前函数返回,所以可能会因为跳过清理代码导致资源泄露。根据泄露的性质,你也许需要或不需要修正这个问题。但无论如何牢记你有可能因为断言错误导致额外的堆检查错误(heap checker error)。

简单的只用流定向操作符"<<"你就可以提供定制的失败信息。

ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";

for (int i = 0; i < x.size(); ++i) {
  EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}

任意能输出到ostream流的内容就能被输出到断言,特别是C的字符串和string对象。如果是宽字符(wchar_t*,Windows下UNICODE模式的TCHAR*,或者std::wstring)内容被输出到断言,它们在打印时会自动被转换成UTF-8编码。

- 基本断言

以下断言用于基本的真/假条件测试。

致命断言 非致命断言 通过条件
ASSERT_TRUE() EXPECT_TRUE(condition) condition为真
ASSERT_FALSE() EXPECT_FALSE(condition) condition为假



记住,当失败的时候,ASSERT_*会产生一个致命失败并从当前函数返回,而EXPECT_*则产生一个非致命失败,并允许程序继续执行。在任何一种情况下,断言失败就意味着测试失败。

在Linux,Windows和Mac上可用。

- 二元比较

这一节列出了用于比较两个值的断言。

致命断言 非致命断言 通过条件
ASSERT_EQ(expected, actual) EXPECT_EQ(expected, actual) expected == actual
ASSERT_NE(val1, val2) EXPECT_NE(val1, val2) val1 != val2
ASSERT_LT(val1, val2) EXPECT_LT(val1, val2) val1 < val2
ASSERT_LE(val1, val2) EXPECT_LE(val1, val2) val1 <= val2
ASSERT_GT(val1, val2) EXPECT_GT(val1, val2) val1 > val2
ASSERT_GE(val1, val2) EXPECT_GE(val1, val2) val1 >= val2







在失败事件中,GTest会打印出val1和val2的值。在ASSERT_EQ*和EXPECT_EQ*以及其它相等性判断断言中,你应该把你要测试的表达式放在actual的位置上,把期望的值放在expected的位置上,因为GTest的失败信息会据此规范进行优化。

值类型必须是断言比较操作符能够处理的类型,不然就会遇到编译错误。我们以前要求参数支持"<<"操作符一边输出到ostream,但是从v1.6.0开始就不再必要了(如果支持"<<"操作符,当断言失败时就会被调用来打印参数,否则GTest就会自动尝试用最好的方式打印参数。关于更多如何自定义打印参数的方法,请参考这篇关于Google Mock的文章。)。

这些断言也可以和用户自定义类型一起工作,但是你必须为这些类型定义对应的比较操作符(例如"==","<"等)。如果以上操作符被定义,最好使用ASSERT_*()系列的宏因为它们不仅打印比较的结果,而且还打印两个操作数。

参数永远只被计算一次。也就数说,参数有副作用也没关系。但是和所有普通的C++函数一样,参数的计算顺序是不固定的(编译器决定),所以你的代码不能依赖于任何特定的参数求值顺序。

ASSERT_EQ()不比较指针的相等性。如果你比较两个C风格的字符串,它只比较它们是否在相同的内存位置,而不是它们的内容是否相同。所以,如果如果你想比较C风格的字符串(例如const char*)的值,就应该调用ASSERT_STREQ(),马上就会被介绍到。特别注意,如果你想判断C风格的字符串是不是一个空指针,请使用ASSERT_EQ(NULL, c_string)。但是,如果比较两个string对象,请使用ASSERT_EQ。

- 字符串比较

以下断言是用来比较两个C风格字符串的。如果你想比较两个string对象,请使用EXPECT_EQ,EXPECT_NE及其它类似函数。

致命断言 非致命断言 通过条件
ASSERT_STREQ(expected_str, actual_str) EXPECT_STREQ(expected_str, actual_str) 两个C风格字符串内容相等
ASSERT_STRNE(str1, str2) EXPECT_STRNE(str1, str2) 两个C风格字符串内容不相等
ASSERT_STRCASEEQ(expected_str, actual_str) EXPECT_STRCASEEQ(expected_str, actual_str) 忽略大小写,两个C风格字符串内容相等
ASSERT_STRCASENE(str1, str2) EXPECT_STRCASENE(str1, str2) 忽略大小写,两个C风格字符串内容不相等





注意断言里的"CASE"表示大小写将被忽略。

*STREQ*和*STRNE*也接受C风格的宽字符串(wchar_t*)。如果两个宽字符串比较失败,它们的值将使用UTF-8窄字符编码被打印输出。

一个空指针和一个空字符串被认为是不相等的。

在Linux,Windows和Mac上可用。

请参考《GTest高级指南》(翻译施工中)来获得更多关于字符串比较的技巧(子字符串,前缀,后缀,正则表达式匹配等)。

简单测试

如何创建一个新的测试:

  1. 使用TEST()宏来定义和命名一个测试函数。它们是普通的C函数并且不返回任何值。
  2. 这个函数里你可以编写任何有效的C++语句,并且使用不同的GTest断言来对值做检查。
  3. 测试结果由断言来决定;如何任何一个断言(致命或非致命)失败,或者测试崩溃了,那么整个测试就失败了,不然就算成功。
TEST(test_case_name, test_name) {
 ... test body ...
}

我们现在由浅入深讲一下TEST()的参数。第一个参数是测试用例的名字,第二个是测试用例中具体测试的名字。两个名字都必须是有效的C++标示符,并且不能包含下划线'_'。一个测试的完成名字包含测试用例的名字和它自己的名字。不同测试用例中的测试可以使用相同的名字。

我们以下面这个返回int的函数为例:

int Factorial(int n); // Returns the factorial of n

这个函数的测试用例可能如下代码所示:

// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput) {
  EXPECT_EQ(1, Factorial(0));
}

// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput) {
  EXPECT_EQ(1, Factorial(1));
  EXPECT_EQ(2, Factorial(2));
  EXPECT_EQ(6, Factorial(3));
  EXPECT_EQ(40320, Factorial(8));
}

GTest根据test cases把测试结果分组,所以逻辑上等价的测试必须放在同一个测试用例下。也就是说,它们TEST()宏的第一个参数必须相等。在上面的例子中,测试HandleZeroInput和HandlePosotiveInput都属于同一个测试用例FactorialTest。

在Linux,Windows和Mac上可用。

Test Fixtures: 在多个测试中使用相同的数据配置

当你发现你编写的多个测试只需要操作相似的数据,你也许应该考虑使用test fixture。这使得你可以在不同的测试中重用相同的数据配置。

创建一个fixture很简单,只要以下几步:

  1. 从::testing::Test集成一个类。根据我们希望访问子类的fixture成员的权限,限定为protected或public。
  2. 在类中,定义任何你想使用的对象。
  3. 如果需要,实现默认的构造函数或SetUp()函数来为每个测试准备数据。一个常见的错误是把SetUp()拼写为Setup,千万注意不要把大写的'U'写成小写的'u'啊。
  4. 如果需要,实现析构函数和TearDown函数来释放SetUp()函数分配的资源。要学习什么时候应该使用构造/析构函数和什么时候使用SetUp()/TearDown()函数,请参考这份FAQ
  5. 如果需要,请自定义测试需要共享的函数。

如果想使用fixture,请使用TEST_F()代替TEST()以便访问test fixture的数据和函数。

TEST_F(test_case_name, test_name) {
 ... test body ...
}

和TEST()的第一个参数用来表示测试名字类似,TEST_F()的第一个名字用来表示test fixture的名字。你也许立刻就猜到了,"_F"用来表示fixture。

另外,在使用TEST_F()前,你要先定义一个test fixture类,不然你就会得到一个编译错误"virtual outside class declaration"。

对于每一个被TEST_F()定义的测试,GTest会做以下工作:

  1. 在运行时创建一个新的test fixture。
  2. 立即调用SetUp()进行初始化。
  3. 运行测试。
  4. 调用TearDown进行清理。
  5. 销毁test fixture。注意相同测试用例下的不同测试使用不同的test fixture对象,GTest总是在创建新的对象前销毁已有的。GTest不会为不同的测试重用相同的test fuxture对象。这样做的好处在于一个测试对fixture做的改动不会影响其它测试。

现在我们来看一个例子,为一个名为Queue的先进先出队列类写一个测试,类的接口如下:

template <typename E> // E is the element type.
class Queue {
 public:
  Queue();
  void Enqueue(const E& element);
  E* Dequeue(); // Returns NULL if the queue is empty.
  size_t size() const;
  ...
};

首先定义一个fixture类,命名为QueueTest。

class QueueTest : public ::testing::Test {
 protected:
  virtual void SetUp() {
    q1_.Enqueue(1);
    q2_.Enqueue(2);
    q2_.Enqueue(3);
  }

  // virtual void TearDown() {}

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

在这个例子中我们没有实现TearDown函数,因为不需要做清理工作,析构函数已经可以解决所有问题。

现在我们开始用TEST_F()来实现测试代码。

TEST_F(QueueTest, IsEmptyInitially) {
  EXPECT_EQ(0, q0_.size());
}

TEST_F(QueueTest, DequeueWorks) {
  int* n = q0_.Dequeue();
  EXPECT_EQ(NULL, n);

  n = q1_.Dequeue();
  ASSERT_TRUE(n != NULL);
  EXPECT_EQ(1, *n);
  EXPECT_EQ(0, q1_.size());
  delete n;

  n = q2_.Dequeue();
  ASSERT_TRUE(n != NULL);
  EXPECT_EQ(2, *n);
  EXPECT_EQ(1, q2_.size());
  delete n;
}

上面我们同时使用了ASSERT_*和EXPECT_*断言。规则是如果我们想通过断言暴露尽可能多的错误,使用EXPECT_*。如果断言失败之后代码继续执行没有意义,使用ASSERT_*。例如,Dequeue测试第二个断言使用"ASSERT_TRUE(n != NULL)",因为我们需要在后面对指针解引用,所以如果它为空的话继续执行就没有任何意义。

当一个测试执行时会发生以下事件:

  1. GTest构造QueueTest对象(我们称之为t1)。
  2. t1.SetUp()函数初始化t1。
  3. t1的第一个测试IsEmptyInitially执行。
  4. 在测试执行完后调用t1.TearDown()清理现场。
  5. t1被析构。
  6. 以上步骤重复一遍,这一轮是执行测试DequeueWorks。

在Linux,Windows和Mac上可用。

注:GTest在创建测试对象时自动保存所有GTest的标志位(flag),在对象销毁后自动恢复这些标志位。

调用测试

TEST()和TEST_F()默认向GTest注册它们的测试。所以和其它C++测试框架不同,你不必为了执行测试而把你定义的测试重新列一遍。

在定义完你的测试后,你可以调用RUN_ALL_TESTS()来执行所有的测试。返回值为0说明全部测试通过,1则说明有失败的测试。注意,RUN_ALL_TESTS()执行你链接在代码里的所有测试,可以来自不同的测试用例,也可以来自不同的源文件。

当被调用时,RUN_ALL_TESTS()宏会执行以下任务:

  1. 保存GTest当前标志位的所有状态。
  2. 为第一个测试创建新的test fixture对象。
  3. 调用SetUp()进行初始化。
  4. 执行测试。
  5. 调用TearDown()进行现场清理。
  6. 销毁fixture对象。
  7. 重置之前保存的GTest的所有标志位。
  8. 重复以上步骤直到完成所有测试执行。

另外,如果第2步在test fixture的构造函数出现致命错误的话,第3到5步将被跳过。如果第3步出现致命错误的话,第4步将被跳过。

重要:千万不要忽略RUN_ALL_TESTS()的返回值,不然gcc会给你一个编译错误。设计时我们决定自动化测试服务根据退出代码判断测试是否通过,而不是stdout/stderr的输出,所以main()函数必须返回RUN_ALL_TESTS()的返回值。

另外你只能调用RUN_ALL_TESTS()一次。多次调用将导致和GTest的一些高级特性冲突(线程安全的死亡测试)并导致这些特性无法正常工作。

在Linux,Windows和Mac上可用。

编写main()函数

你可以先从这个模板文件起步:

#include "this/package/foo.h"
#include "gtest/gtest.h"

namespace {

// The fixture for testing class Foo.
class FooTest : public ::testing::Test {
 protected:
  // You can remove any or all of the following functions if its body
  // is empty.

  FooTest() {
    // You can do set-up work for each test here.
  }

  virtual ~FooTest() {
    // You can do clean-up work that doesn't throw exceptions here.
  }

  // If the constructor and destructor are not enough for setting up
  // and cleaning up each test, you can define the following methods:

  virtual void SetUp() {
    // Code here will be called immediately after the constructor (right
    // before each test).
  }

  virtual void TearDown() {
    // Code here will be called immediately after each test (right
    // before the destructor).
  }

  // Objects declared here can be used by all tests in the test case for Foo.
};

// Tests that the Foo::Bar() method does Abc.
TEST_F(FooTest, MethodBarDoesAbc) {
  const string input_filepath = "this/package/testdata/myinputfile.dat";
  const string output_filepath = "this/package/testdata/myoutputfile.dat";
  Foo f;
  EXPECT_EQ(0, f.Bar(input_filepath, output_filepath));
}

// Tests that Foo does Xyz.
TEST_F(FooTest, DoesXyz) {
  // Exercises the Xyz feature of Foo.
}

}  // namespace

int main(int argc, char **argv) {
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

::testing::InitGoogleTest()函数解析从命令行输入的GTest标志位,读取完成后把以识别的标志位从命令行中删除。这使得用户可以使用不同的标志位控制程序的行为,在《GTest高级指南》(翻译施工中)我们会做进一步讨论。在调用RUN_ALL_TEST()之前你必须调用这个函数,不然标志位不会被正确初始化。

在Windows平台上,InitGoogleTest()可以和宽字符串一起工作,所以可以和程序一起用UNICODE模式编译。

也许你会认为写main()函数太麻烦了,我们完全同意,所以在GTest里提供了一个main()函数的基本实现。如果你觉得这个main函数够用了,编译时请链接gtest_main库。

- VC用户的重要注意事项

如果你把测试放在一个库里,而main函数在你的.exe文件的另一个库里,那么这些测试不会被执行。这是Visual C++的一个bug。当你定义测试时,GTest会创建某种静态对象来注册它们。这些对象不会在其它地方被引用但是它们的构造函数理论上还是会运行。但是Visual C++的linker看到这些库里的对象没有被任何地方引用时,它就把这个库扔掉了。所以你必须在你的主程序中引用你库里的测试,这样linker就不会忽略它们了。这里是具体做法。在你的库代码中加上这么一段:

__declspec(dllexport) int PullInMyLibrary() { return 0; }

如果你使用静态链接库的话, __declspec(dllexport)可以省略。现在在你的主程序里加上这么一段代码:

int PullInMyLibrary();
static int dummy = PullInMyLibrary();

这样你的测试就会被引用,而且在程序开始的时候就会被注册。

另外,如果你在静态链接库中定义测试,请在你的主程序链接选项中加入"/OPT:NOREF"。如果你使用MSVC++ IDE,请在你的.exe项目properties/Configuration Properties/Linker/Optimization的引用设置中选中Unreferenced Data(/OPT:NOREF)。这样可以阻止Visual C++的linker在最终生成的可执行文件中忽略掉与你的测试相关的符号。

还有另外一个缺点。如果你把GTest编译为静态库(在gtest.vcproj中就是这么定义的),那么你的测试也必须存在于静态库中。如果你使用动态链接库DLL的话,你必须把GTest也编译为动态链接库。不然你的测试就不会运行或运行错误。一个简单的结论是:想要你的生活更容易,不要把你的测试放在单独的库里。

下一步往去哪儿

恭喜!你已经掌握GTest的基本知识了。现在你可以开始用GTest编写和运行你自己的测试,接下来,可以参考更多例子,或者探索《GTest高级指南》(翻译施工中)以发现更多有用的特性。

已知局限性

GTest被设计成线程安全的。如果系统提供pthreads库的话,那么实现就是线程安全的。目前在两个并发的线程同时使用断言在某些系统(比如Windows)上是不安全的。对大多数测试来说这不是一个问题,因为大多数断言在主线程内完成工作。如果你愿意帮助,你可以志愿为你的平台在gtest-port.h里实现必要的同步原语。

posted @ 2013-11-15 13:36  移山测试工作室黑灯老师  阅读(9873)  评论(1编辑  收藏  举报
count website visits
Buy Computers