Google C++ 测试框架

简介:为什么是Google C++测试框架?

Google C++测试框架能帮助你写出更好的C++测试代码。

不管你是基于Linux、Windows或者Mac,只要你是写C++代码,Google Test都能帮到你。

那什么是一个好的测试呢?Google C++测试框架又如何去适应它呢?我们相信:
1.测试应该是独立的可重复的。像调试其它测试的结果一样,调试一个结果为成功或者失败的测试是一件痛苦的事情。Google C++测试框架通过使每个测试运行在不同的对象中从而使测试隔离。当一个测试失败时,Google C++测试框架允许你将它运行在隔离的环境下从而达到快速调试的目的。
1.测试应该要有良好的组织以反映被测试代码的结构。Google C++测试框架将相关的测试划分到一个测试组内,并且测试组内的测试能共享数据和子例程。这种普通的模式易于识别,也使测试易于维护。 当需要切换项目并且在一个新的代码库中开展工作的时候,这种一致性特别有用。
1.测试应该是可移植的可复用的。开源社区有很多与平台无关的代码,那么它的测试代码也应该要与平台无关的。Google C++测试框架能在不同的操作系统下工作,并且支持不同的编译器(gcc、MSVC,还有其它),包括支持带或者不带异常处理的编译器,所以Google C++测试框架下的测试能在各种各样的配置环境下工作。(注意,当前发布的版本仅包含支持Linux的构建脚本,我们正在努力为其它平台提供构建脚本)
1.当测试失败时应该尽可能地提供关于问题的信息。Google C++测试框架不会因为碰到第一个测试失败就停止,而是仅仅停止当前的测试并且接着下一个测试。你能把测试设置成报告非致命错误后继续执行。因此你能在单个运行-编辑-编译的周期内发现并修复多个bug。
1.测试框架让测试人员不再需要编写那些琐碎的代码,而是让他们专注于测试内容。Google C++测试框架能自动跟踪所有已定义的测试,不需要用户为了执行它们而去重新列举它们。
1.测试要求快速。在Google C++测试框架下,你能在测试之间重用共享的资源,并且仅仅执行set-up/tear-down一次,使测试不依赖于其它。

因为Google C++测试框架是基于当前流行的xUnit架构,如果你之前已经使用过JUnit或者是PyUnit的话,你会感觉到很熟悉。如不是的话,将会花费你大约10分钟的时间来学习基础知识。让我们开始吧!

注意:我们有时候会把Google C++测试框架简称为Google Test

建立一个新的测试项目

为了能使用Google Test来写测试程序,你需要把Google Test编译成库并且链接到你的测试程序。我们提供了多个构建文件来支持流行的构建系统,例如Visual Studio的msvc/、Mac Xcode的xcode/、GNU make的make/、Borland C++ Builder的codegear/,并且在Google Test的根目录下提供了autotools脚本文件(已废弃)和用于CMake的CMakeLists.txt(推荐)。如果你的构建系统不在这个列表中,那么你可以参考make/Makefile来学习如何编译Google Test(主要是你要编译src/gtest-all.cc,要把GTEST_ROOTGTEST_ROOT/include包含在头部搜索路径,GTEST_ROOT就是Google Test的根目录)。

一旦你编译了Google Test库,那么你就应该为你的测试程序创建一个项目或者构建目标。必须要保证GTEST_ROOT/include包含在头部搜索路径,以便于编译器在编译你的测试程序时能搜索到"gtest/gtest.h"文件。将Google Test库链接到你的测试项目(例如在Visual Studio,通过添加对gtest.vcproj的依赖来完成)。

如果你还是有疑问的话,你可以看一下Google Test本身的测试是如何构建的,并且可以用它们作为例子。

基本概念

当你使用Google Test时候,必须要从写断言开始。断言其实就是检测条件是否为真的语句。一个断言的结果可是成功非致命失败致命失败。如果发生致命失败,那么将会异常终止当前函数,否则程序将会继续正常运行。

测试使用断言来验证被测试代码的行为。如果一个测试崩溃了或者产生一个失败断言,那么就意味着失败了,否则就是成功

一个测试组包含一个或者多个测试。你应该要把你的测试划分到测试组中,这样子通过测试组来反映被测试代码的结构。当一个测试组中有多个测试需要共享公用的对象或者例程时,那么你能把它们放到一个测试夹类中。

一个测试程序能包含多个测试组。

我们现在来解释如何写一个测试程序,通过简单的断言来开始构建测试和测试组。

断言

Google Test的断言其实是长得像函数调用的宏定义。测试一个类或者函数的时候你可以根据它们的表现来写断言。当一个断言失败时,Google Test会打印断言的失败信息所在的源文件和行号位置。你还能在Google Test的信息后面追加一个自定义的失败信息。

断言都是呈多对的形式出现的,而这种断言在当前的函数下测试相同的事件却会有不同的效果。ASSERT_*版本的断言当失败时会产生致命失败,并且会终止当前函数EXCEPT_*版本的断言会产生非致命失败,但是不会终止当前函数。通常情况下建议使用EXCEPT_*,因为它们允许在一个测试中不只报告一个失败。然而当有问题的断言失败时并且没有继续执行的意义时候,你应该使用ASSERT_*

因为一个失败的ASSERT_*会从当前函数立即返回,所以可能会跳过紧跟它的清理代码,这可能会导致内存泄露。这取决于内存泄露的意义来判断这是不是一个值得修复的问题。如果除了断言错误之外还有堆检查程序错误时,那么你就要牢记了。

为了提供一个自定义的失败信息,可以通过使用<<操作符简单地把自定义的失败信息以流的形式追加到宏定义后,还可以使用一序列这样的操作符。例子如下:

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对象。如果一个宽字符串(windows下的UNICODE形式的wchar_t*TCHAR*,或者是std::wstring)被以流的形式追加到一个断言,在打印到屏幕的时候会被转换成UTF-8形式。

基本断言

这些断言完成基本的true/false条件测试。

Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false

ASSERT_*失败时会产生一个致命失败并且从当前函数返回,而EXPECT_*产生一个非致命失败时允许函数继续执行。无论是什么情况 ,一个断言失败就意味着它包含的条件失败。

支持平台:Linux、Windows、Mac。

二进制比较

这部分讲述的断言用于比较两个值。

Fatal assertion Nonfatal assertion Verifies
ASSERT_EQ(val1,val2); EXPECT_EQ(val1,val2); val1 == val2
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

在断言失败的情况下,Google Test会打印val1val2的值。

值参数必须是可比较的,也就是说能够用于断言的比较运算符,否则你将会得到一个编译错误。我们过去常常要求参数要支持<<运算符来实现流插到ostream对象,不过从v1.6.0开始不再要求了(如果支持<<的话,那么当断言失败时会打印参数;否则Google Test会试图用最恰当的方式来打印它们。想了解更多的详细和如何自定义打印参数,请看Google Mock recipe.).

这些断言虽然支持用户定义类型,但是需要你定义与比较运算符相应的比较规则(例如==,<等等)。如果定义了相应的比较规则,那么使用ASSERT_*()会更好,因为它们不仅会打印比较结果,还会打印两个操作数。)

参数总是只计算一次,无论参数是否有副作用。然而跟普通的C/C++函数一样,参数的计算顺序是不确定的(也是是编译器可以任意选择一种计算顺序),所以你的代码不能依赖于任何一种特定的参数计算顺序。

ASSERT_EQ()用于指针的相等比较。如果是用于两个C字符串,则会测试它们是否在相同的内存位置而不是拥有相同的值。如果你是想比较C字符串(例如const char*)的值,可以使用ASSERT_STREQ()(稍后会介绍)。特别是为了判断C字符串为空,可以使用ASSERT_STREQ(NULL,c_string)。然而比较两个字符串对象,则使用ASSERT_EQ

这部分的宏定义能用于普通字符串对象和宽字符串对象(stringwstring)。

支持平台:Linux、Windows、Mac.

历史记录:在2016年2月之前,关于调用*_EQ都有一个约定,就是ASSERT_EQ(expected, actual),所以很多代码都是使用这种顺序。但是现在*_EQ则以相同的方式处理这两个参数。

字符串比较

这个分组内的断言是用于比较比较两个C字符串。如果你想比较两个string对象,可以使用EXPECT_EQ,EXPECT_NE等等。

Fatal assertion Nonfatal assertion Verifies
ASSERT_STREQ(str1,str2); EXPECT_STREQ(str1,_str_2); the two C strings have the same content
ASSERT_STRNE(str1,str2); EXPECT_STRNE(str1,str2); the two C strings have different content
ASSERT_STRCASEEQ(str1,str2); EXPECT_STRCASEEQ(str1,str2); the two C strings have the same content, ignoring case
ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2); the two C strings have different content, ignoring case

注意了,在断言名中的”CASE”意味着忽略大小写。

*STREQ**STRN*也接受宽C字符串(wchar_t*)。如果比较两个宽字符串失败,那么它们的值将会以UTF-8的字符串形式打印出来。

NULL指针和空字符串是两个不同的东西。

支持平台:Linux,Windows,Mac .

参见:如果想了解更多关于字符串比较的技巧(例如substring,prefix,suffix和正则表达式匹配),可以参考Advanced Google Test Guide。

简单测试

创建一个测试:
1、使用TEST()宏来定义和命名一个测试函数,这是一个普通的C++函数,不需要返回值。
2、在函数体内,你可以包含所有合法的C++语句,并且使用各种Google Test断言来检查值。
3、测试的结果由断言决定;如果在测试中的任何一个断言失败了(不管是严重或者非严重)或者测试崩溃了。那么就表示整个测试失败了,否则为成功。

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

TEST()的参数从一般到具体。第一个参数是测试组的名称,第二个参数是测试组内测试的名称。这两个名称必须是合法的C++标识符,并且它们不能含有下划线(_)。一个测试的全名由它包含的测试组名和它独有的名称组成的。不同的测试组可以有相同的独有名称。

让我们从一个简单的整型函数开始:

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));
}

Google Test使用测试组来对测试结果进行划分,所以逻辑上相关的测试应该放入相同的测试组中;换句话说,就是它们TEST()的第一个参数应该是一样的。正如上面的例子,我们有两个测试,HandlesZeroInputHandlesPositiveInput,它们都属于相同的测试组FactorialTest

支持平台:Linux、Windows、Mac.

测试夹:对多个测试使用相同的数据配置信息

如果你发现自己写的两个或者更多的测试操作的数据都是相似的,那么你可以使用测试夹。它允许你对多个不同的测试重用相同的对象配置信息。

To create a fixture, just:
创建一个测试夹只需要:
1.从::testing::Test中派生一个类,并且以protected:或者public:作为派生类的开头,因为这样子我们才能在子类中访问父类的成员。
1.在类中你可以声明你打算使用的对象。
1.如果有必要,你可以为每个测试写一个默认的构造函数或者SetUp()函数来初始化对象。一个很常见的错误就是把SetUp()拼写成小写uSetup()-不要让这些事件发生在你身上。
4.如果有必要,你可以写一个析构函数或者TearDown()函数来释放在SetUp()中分配的资源。如果想知道什么时候应该用构造函数/析构函数和什么时候应该用SetUp()/TearDown()函数,可以参考FAQ entry。
5.如果有需要,为你的测试定义共享的子例程。

当你使用一个测试夹时,那么应该使用TEST_F()而不是TEST()。因为TEST_F()允许你访问在测试夹内的对象和例程。

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

TEST()相似,TEST()的第一个参数是测试组名,而TEST_F()第一个参数是测试夹类的名称。你可能应该猜到,_F其实就是fixture。

不幸的是C++宏系统不允许我们创建一个能处理这两种测试类型的简单宏定义。使用错误的宏会造成编译错误。

还有的是你在TEST_F()内使用测试夹类前必须先定义,否则会产生编译错误“virtual outside class declaration”。

对于每个用TEST_F()定义的测试,Google Test将会:
1.在运行时创建一个fresh测试夹
1.并且立即调用SetUp()初始化它,
1.运行测试
1.调用TearDown()进行清理
1.删除测试夹。注意了,在相同的测试组中的不同测试都有不同测试夹对象,所以Google Test总是会在创建下一个测试夹前删除当前的测试夹。对于多个测试,Google Test不会重用相同的测试夹。Any changes one test makes to the fixture do not affect other tests.

举个例子,为一个名为Queue的FIFO队列类写测试,它有以下接口:

template  // 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;
  ...
};

首先定义一个测试夹类。一般情况下,如果要测试的类是Foo,那么会把这个测试夹类命名为FooTest

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),因为我们需要对n指针进行解引用时因为nNULL会导致一个段失败。

当运行这些测试,会发生以下流程:
1.Google Test构造一个QueueTest对象(命名为t1)。
1.调用t1.SetUp()初始化t1
1.在t1上执行第一个测试(IsEmptyInitially)。
1.测试完成后调用t1.TearDown()进行清理。
1.t1被销毁。
1.在另一个QueueTest对象中重复以上步骤,这次是运行DequeueWorks的测试。

支持平台:Linux,Windows,Mac.

注意:Google Test在构造一个测试对象时会自动保存所有Google Test的标志状态,并且在该对象销毁时还原标志状态。

调用测试

TEST()TEST_F()会隐式地把它们的测试注册到Google Test。所以不像许多其它C++测试框架那样,为了运行你定义的所有测试,必须要你把它们重新列举一次。

定义好你的测试后,通过调用RUN_ALL_TESTS()来运行它们,如果测试成功则函数返回0,否则返回1。注意的是RUN_ALL_TESTS()会运行你的链接单元中的所有测试。它们可能来自不同的测试组,甚至是来自不同的源文件。

当调用RUN_ALL_TESTS()宏时:
1.保存所有Google Test标志状态。
1.为第一个测试创建一个测试夹对象。
1.通过SetUp()初始化。
1.在测试夹对象上运行测试。
1.通过TearDown()进行清理测试夹对象。
1.删除该测试夹对象。
1.还原所有Google Test标志状态。
1.对下一个测试重复上述步骤,直到所有测试都运行了。

另外的是,如果在第2步测试夹的构造函数产生致命失败的话,那么第3-5步就没有意义了,所以它们会因此被跳过。如果第3步生成致命失败,那么会路过第4步。

重点:你切不可忽略RUN_ALL_TESTS()的返回值,不然gcc会报告编译错误。这种设计的原理是自动化测试服务判断一个测试是否已经通过是基于它的程序退出码而不是它的stdout/stderr输出;因此你的main()函数必须要返回RUN_ALL_TESTS()的值。

而且你仅需要调用RUN_ALL_TESTS()一次。调用多次会与一些高级的Google Test功能产生冲突,因此不支持这种操作。

支持平台: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()函数解析命令行以用于Google Test标志,并且移除所有已识别的标志。这样子就能允许用户通过设置各种各样的标志(在高级手册中覆盖)来控制测试程序的行为。你必须在调用RUN_ALL_TESTS()前调用这个函数,否则这些标志就可能没有被正确初始化。

在Windows环境下,InitGoogleTest()也支持宽字符串,所以它也能用于UNICODE模式编译的程序。

但是,你可能认为写那些main()函数的工作量很大。我们完全认同你的想法,所以这就是为什么Google Test提供一个基础的main()实现。如果它符合你的需要,那么你只需要把gtest_main库链接到你的测试中,这样子就可以了。

针对Visual C++用户的重要提示

如果你把你的测试放到一个库里,并且你的main()函数是位于不同于测试的那个库或者是在你的.exe文件的地方,那么那些测试是不会执行的。原因是Visual C++的bug。当你定义你的测试时,Google Test会创建明确的静态对象并注册它们。这些对象没有从其它地方引用过,但是它们的构造函数却仍然执行。当Visual C++链接器发现库里没有东西被其他地方引用,那么链接器就会忽略该库。你不得不在你的测试程序中引用你的库来防止链接器丢弃你的库。解决的办法是在你的库的代码的某地方声明一个函数:

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

如果你把你的测试放到一个静态库(非DLL),那么__declspec(dllexport)就不需要了。现在在你的主程序中写一段代码调用那个函数:

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

这样就能保持你的测试被引用并且它们会在启动时注册自己。

另外,如果你在一个静态库中定义你的测试,把/OPT:NOREF添加到你的主程序链接器的选项中。如果使用MSVC++IDE,定位到你的.exe的项目属性/配置属性/链接器/优化,然后把引用设置设置成Keep Unreferenced Data(/OPT:NOREF)。这防止Visual C++链接器在生成最终的可执行程序时丢弃你的测试生成的单独符号文件。

如果你把Google Test作为静态库使用(那要看在gtest.vcproj怎样定义了),那么你的测试必须驻留在静态库中。如果你不得不要把测试放在DLL里,那么你必须要把Google Test也构建到DLL。否则你的测试不能正确运行或者是根本运行不了。总结一下:如果你想轻松一点,那就不要把你的测试写进库里。

何去何从

贺喜你!你已经学习了Google Test基础教程。你能开始写Google Test测试并且让它跑起来,你可以阅读一些示例或者进一步阅读高级手册,里面会介绍许多更加有用的Google Test功能。

已知的限制

Google Test是线程安全的。这种线程安全的实现需要系统支持pthreads库。在其它一些系统(例如Windows)上如果有两个线程同时使用Google Test断言,那么这样子会导致不安全。其实在大部分测试中这不算是个问题。因为断言通常都会在主线程中完成的。如果你想改善的话,那么你可以无偿去为你的平台实现一些在gtest-port.h中定义的同步原语。

你可能感兴趣的:(源代码分析,谷歌,测试,C++)