Google Test-Primer

引言:为什么使用Google测试框架

Google测试框架有助于编写更好的C++测试。

不论你是工作在Linux,Windows或者是Mac上,如果你写C++代码,Google测试框架都可以帮助你。

然后,什么是好的测试以及Google测试框架如何胜任这个工作,我们相信:

  1. 测试必须是独立可重复。调试一个基于其它测试来决定成功或者失败的测试是痛苦的。Google C++测试框架在不同对象上对立的运行每个测试。当其中一个测试失败后,Google C++测试框架允许你单独运行它,以方便快速调试。

  2. 测试必须组织良好,反应被测试代码的结构。Google C++测试框架将相关的测试组合进测试案例中,他们可以共享数据和子进程。这种通用的模式很常见,而且很容易维护。这种一致性对哪些在不同项目中切换,并且基于新的代码来工作的人尤其有帮助。

  3. 测试必须是可移植和可重复使用的。开源社区中很多代码都是平台无关的,它的测试必须也是平台无关的。Google C++测试框架可以工作在不同系统上不同编译器(GCC,MSVC以及其它的)上,允许异常或者不允许异常,所以Google C++测试框架可以很轻松在不同配置环境中工作(目前的发行版本只包括在Linux下工作的脚本,我们正积极编写在其它平台上的脚本)。

  4. 当测试失败后,他们必须提供尽可能多的关于问题的信息。Google C++测试框架不会在第一个测试失败后停止。取而代之的是,它只会停止当前的测试,继续执行下一个测试。你可以配置测试选项来报告非致命错误,然后继续执行当前测试。因此,你可以在一个run-edit-compile循环中发现和修复很多bug。

  5. 测试框架必须将测试代码编写从日常零星工作中解放出来,将更多的精力专注与测试内容。Google C++测试框架自动根据所有定义的测试,不需要用户为了运行测试而必须枚举每个测试。

  6. 测试必须快。使用Google C++测试框架,你可以在不同测试间重用共享的资源,为setUp和tearDown支付一次代价即可,而且不用让不同测试相互依赖。

因为Google C++测试框架是依据流行的xUnit结构,如果你以前使用过JUnit或者PyUnit,你会觉得很熟悉。如果没有使用过的话,你可以在10分钟内完成基础的学习,并且开始编写自己的测试。Let's Go!

Note: 我们有时候非正式地称Google C++ Test Framework为Google Test。

扫描二维码关注公众号,回复: 10147932 查看本文章

建立新的测试工程

为了使用Google Test测试程序,你需要将Google Test编译成库,然后将你的测试和库链接。我们在Google Test的根目录下提供了流行的编译系统的编译文件: MSVC/visual studio, xcode/ MAC, make/GNU,codegear/Borland C++ Builder,以及自动化脚本(deprecated)  CMakeLists.txt(推荐)。如果你的编译系统不在上述列表中,你可以阅读一下make/Makefile,学习Google Test是如何被编译的(一般情况下,你需要编译src/gtest-all.cc,将GTEST_ROOT和GTEST_ROOT/include加入头文件搜索目录,GTEST_ROOT是Google Test的根目录)。

一旦你可以编译Google Test库,你可以创建你的测试程序的工程。确保将GTEST_ROOT/include 包含进头文件查找路径,这样你的编译器在编译你的测试程序时可以查找到gtest/gtest.h。设置你的工程和Google Test库链接(例如在visual studio中,增加gtest.vcproj的依赖)。

如果你仍然有问题,看看Google Test自己的测试是如何编译的,并且把他们作为例子。

基本概念

当使用Google Test时,从写断言开始,他们是判断一个条件是否是真。一个断言结果可以是成功、非致命性失败或者是致命性失败。当致命性失败发生时,当前函数会终止,否则程序正常继续。

测试使用断言来正式被测代码的行为。如果测试失败或者断言失败,那么测试失败,否则它成功

一个测试程序(Test Program)包括多个测试案例(Test Case)。

我们从单独的断言开始,一直到构建测试和测试案例来解释如何写测试代码。

断言

Google Test断言是利用宏来模拟函数调用。你可以通过对齐行为作为断言来测试类或者函数。当断言失败时,Google Test打印出断言失败的源文件和行的位置,以及一条失败信息。你可以提供自定义的失败信息,这些信息会跟在Google Test信息之后。

断言是成对出现的,他们测试相同的东西但是有不同的效果。ASSERT_*版本的断言当他们失败时产生致命错误,并且停止当前函数。EXPECT_*版本断言产生非致命性失败,但是不退出当前函数。通常更推荐EXPECT_*,因为他们允许在一个测试中报告多个错误。然而如果断言失败后,继续执行不合理时时必须使用ASSERT_*版本断言。

因为失败的ASSERT_*立刻从当前函数返回,所以可能会跳过后面一些垃圾清理代码,进而导致内存泄漏。根据泄漏的原因,可能值得也可能不值得修复这个bug-所以记住除了断言错误之外,有可能发现堆检查错误时。

为了提供定制的失败信息,可以简单的使用流操作符,或者一些列诸如此类的操作符。下面是一个例子:

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

  2.  
  3. for (int i = 0; i < x.size(); ++i) {

  4. EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;

  5. }

任何可以流向ostream的对象都可输出到ostream,特别是指C字符串和string对象。如果宽字符(wchar_t, TCHAR*或者std::wstring)输出至断言,会被转换成UTF-8来打印。

基本断言

Fatal assertionNonfatal assertionVerifiesASSEERT_TRUE(condition)EXPECT_TRUE(condition)condition is trueASSEERT_FALSE(condition)EXPECT_FALSE(condition)condition is false

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

Avaibility: Linux, Windows, Mac

Binary Comparison

这个部分描述比较两个值大小的断言

Fatal assertion   Nonfatal assertion Verifies 

ASSERT_EQ(expected,actual)  EXPECT_EQ(expected,actual) expected == actual 

ASSERT_NE(val1,val2) EXPECT_NE(val1,val2val1 != 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,val2val1 >= val2

当失败事件发生时,Google Test打印出val1val2。在ASSERT_EQ和EXPECT_EQ(以及后面我们将要介绍的等式断言)中,要将要测试的数据放在actual位置,将期望得到的值放在expected位置,因为Google Test的失败信息会根据这个惯例来优化处理

值参数必须在断言操作符中具有相应的可比性,否则编译器会报错。我们之前要求参数必须支持<<运算符,这样输出到ostream,但是自从1.60版本以后就不需要(如果支持<<运算符,那么在打印参数时会调用,不然Google Test会尝试以最佳的方式打印。更多的信息查阅Google Mock中自定义打印参数的页面)

这些断言可以应用于用户自定义的类型,但是前提是你必须定义相应的比较运算符(例如,==,<等)。如果相应的比较运算符号定义了,那么推荐使用assert_*,因为它不仅打印比较的结果,也会打印两个操作数<>

参数总是计算一次。因此,如果参数具有副作用也没有问题。然而,对于普通的C/C++函数,参数计算的顺序是没有定义的,(编译器自由选择任何顺序),你的代码不应该依赖任何特定参数求值顺序

ASSERT_EQ可以比较指针是否相等。如果比较两个C类型字符串,它检测他们是否指向相同的内存位置,而不是他们是否含有相同的值。所以,如果你想比较两个C字符串(例如const char*)的值,使用ASSERT_STREQ(),后面将会介绍。特别是,判断一个C字符串是否是NULL,使用ASSERT_STREQ(NULL, c_string)。然而,比较两个string对象,使用ASSERT_EQ>

这部分的宏对于宽字符和窄字符同样使用(string和wstring)

Avaibility: Linux, Windows, Mac

String Comparison

这组的断言是用来比较两个C字符串。如果你想比较两个string对象,使用EXPECT_EQ,EXPECT_NE以及其它来代替

Fatal assertion Nonfatal assertion 

Verifies

ASSERT_STREQ(expected_str,actual_str) EXPECT_STREQ(expected_str,actual_str

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(expected_str,actual_str) EXPECT_STRCASEEQ(expected_str,actual_str

the two C strings have the same content, ignoring content

ASSERT_STRCASENE(str1,str2) EXPECT_STRCASENE(str1,str2

the two C strings have different content, ignoring case

注意带CASE的断言语句是忽略大小写的

*STREQ*和*STRNE*同样接收宽字符传。如果两个宽字符串比较失败了,他们值会以UTF-8的窄字符形式打印

一个NULL指针和空字符串是不同的

Avaibility: Linux, Windows, Mac

Simple Tests

建立一个测试

  1. 使用TEST()宏来定义和声明一个测试函数。他们是普通的C++函数,没有返回值

  2. 在这个函数中,可以包含任何合法的C++语句,使用不同Google Test断言来检查值

  3. 测试的结果由断言语句决定。如果任何一个断言失败(不管是致命性的还是非致命性的),或者测试崩溃了,整个测试失败;否则,测试成功。

 
  1. TEST(test_case_name, test_name) {

  2. //....test body....

  3. }

TEST()的参数是从一般到特殊。第一个参数是测试范例的名字,第二个参数是测试范例中某个测试的名字。这两个名字都必须是合法的C++标识符,而且不应该包含下划线。一个测试的全名有测试范例和其本身的名字组成。不同测试范例的测试可以有相同的名字

举个例子,写一个普通的整型函数

int factorial(int n); // return the factorial of n

这个函数的一个测试范例可能会如下所示:

 
  1. // Tests factorial of 0.

  2. TEST(FactorialTest, HandlesZeroInput) {

  3. EXPECT_EQ(1, Factorial(0));

  4. }

  5.  
  6. // Tests factorial of positive numbers.

  7. TEST(FactorialTest, HandlesPositiveInput) {

  8. EXPECT_EQ(1, Factorial(1));

  9. EXPECT_EQ(2, Factorial(2));

  10. EXPECT_EQ(6, Factorial(3));

  11. EXPECT_EQ(40320, Factorial(8));

  12. }

Google Test根据测试范例来对测试分组,所以逻辑相关的测试应该属于相同的测试范例。换句话说,他们的TEST函数的第一个参数是相同的。在上面的例子中,我们有两个测试,HandlesZeroInput和HandlesPositiveInput,他们都属于FactorialTest测试范例。

Avaibility: Linux, Windows, Mac

测试套装(Test Fixtures):为多个测试,使用相同的数据配置

如果你发现自己为两个或以上的测试使用相同的数据,那么你可以使用测试套装。它允许你在不同的测时间重用相同的对象配置。

建立一个测试套装

  1. 从::testing::Teset派生一个类,利用protected或者public开始类,因为我们需要利用子类访问成员。

  2. 在类的内部,声明你准备使用的对象

  3. 如果需要的话,可以写默认的构造函数,或者SetUp()函数,以在每个测试开始前做好准备。一个常见错误是把SetUp()拼成Setup()。小写的u没有这样的作用

  4. 如果需要的话,写析构函数或者TearDown()函数来释放经由SetUp()申请的资源。什么时候使用构造函数与析构函数,什么时候使用SetUP()和TearDown()。请参阅FAQ页面。

  5. 如果需要,定义测试函数之间共享的子程序。

当你使用套装时,利用TEST_F()宏代替TEST()宏,因为它允许你访问对象,以及套装中的子程序。

 
  1. TEST_F(test_case_name, test_name) {

  2. //...test_body....

  3. }

就像TEST()一样,第一个参数是测试范例的名字,但是对于TEST_F()来说,这个名字必须是测试套装的类名。你也许猜到了_F是指测试套装中的Fixture

不幸的是,C++宏系统不允许使用一个宏来处理两种类型的测试。宏的错误使用会导致编译器报错。

同样,你在使用TEST_F()之前,必须定义一个测试套装。不然你会得到编译错误"virtual outside class declaration"

对于每一个定义TEST_F()的测试,Google Test会

  1. 在运行时简历全新的测试套装

  2. 通过SetUp()函数立即初始化

  3. 运行测试

  4. 使用TearDown()函数进行清洁工作

  5. 删除测试套装。注意相同测试范例中的不同测试具有不同的测试套装对象。Google Test在建立一个新的测试套装前会删除当前的。Google Test对不同的测试不复用测试套装。不同测试对测试套装的修改互相不影响。

作为例子,写一个FIFO的队列类,类名为Queue,接口如下

 
  1. template <typename E> // E is the element type.

  2. class Queue {

  3. public:

  4. Queue();

  5. void Enqueue(const E& element);

  6. E* Dequeue(); // Return NULL if the queue is empty.

  7. size_t size()const;

  8. // ...

  9. };

首先定义一个组装类。根据惯例,如果Foo是待测试的类,那么FooTest是测试套装的类的名字。

 
  1. class QueueTest: public ::testing::Test {

  2. protected:

  3. virtual void SetUp() {

  4. q1_.Enqueue(1);

  5. q2_.Enqueue(2);

  6. q2_.Enqueue(3);

  7. }

  8.  
  9. // virtual void TearDown() {}

  10. Queue<int> q0_;

  11. Queue<int> q1_;

  12. Queue<int> q2_;

  13. };

在这个例子中,TearDown()函数不需要因为在每个测试后不需要做清理工作,因为已经由析构函数完成。

现在,我们来使用TEST_F()和这个套装。

 
  1. TEST_F(QueueTest, IsEmptyInitially) {

  2. EXPECT_EQ(0, q0_.size());

  3. }

  4.  
  5. TEST_F(QueueTest, DequeueWorks) {

  6. int* n = q0_.Dequeue();

  7. EXPECT_EQ(NULL, n);

  8.  
  9. n = q1_.Dequeue();

  10. ASSERT_TRUE(n != NULL);

  11. EXPECT_EQ(1, *n);

  12. EXPECT_EQ(0, q1_.size());

  13. delete n;

  14.  
  15. n = q2_.Dequeue();

  16. ASSERT_TRUE(n != NULL);

  17. EXPECT_EQ(2, *n);

  18. EXEPCT_EQ(1, q2_.size());

  19. delete n;

  20. }

上面使用ASSERT_*和EXPECT_*两种断言。使用的准则是:当你想反映出更多问题要继续测试时,使用EXPECT_*;当断言失败后继续测试不合理时,使用ASSERT_*。例如,在第二个断言中,Dequque的测试ASSERT_TRUE(n != NULL),因为我们需要对指针解引用,当n是NULL时,会导致段错误。

当这些测试运行时,如下事件发生

  1. Google Test构建一个QueueTest对象,成为t1

  2. t1.SetUp()初始化t1

  3. 第一个测试(IsEmptyInitially)在t1上执行

  4. 当第一个测试运行结束后t1.TearDown()执行清理

  5. t1被析构

  6. 上述过程重复进行,这次运行DequeWorks测试

Avaibility: 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的所有标记

  2. 为第一个测试建立测试套装

  3. 通过SetUp()初始化

  4. 在测试套装上运行测试

  5. 通过TearDown()清理套装

  6. 删除套装

  7. 重新回复Google Test的标记

  8. 重复以上步骤,运行下一个测试,直至所有测试都运行结束

除此之外,如果在第二步中,测试组装的构造函数产生致命性失败,后面3-5步骤没有再继续的必要,他们将被跳过。同样的步骤3产生致命性错误,步骤4会被跳过。

重要:千万不要忽略RUN_ALL_TESTS()的返回值,不然Gcc会报错。原因是自动测试服务决定测试是否通过是根据返回值来确定的,不是根据stdout/stderr的输出。所以,你的main()函数必须返回RUN_ALL_TESTS()的值。

而且,你只能调用RUN_ALL_TESTS()一次。多次调用会与许多Google Test的高级特性冲突(线程安全的死亡测试等),所以不支持。

Avaibility: Linux, Windows, Mac

写main()函数

你可以从以下模版开始

 
  1. #include "this/packae/foo.h"

  2. #include "gtest/gtest.h"

  3.  
  4. namespace {

  5. class FooTest: public ::testing::Test {

  6. protected:

  7. // you can remove any or al of the following functions if its body

  8. // is empty;

  9. FooTest() {

  10. // You can do set-up work for each test here.

  11. }

  12. virtual ~FooTest() {

  13. // You can do clean-up work that doesn't throw execptions here

  14. }

  15.  
  16. // if the constructor and destructor are not enough for setting up

  17. // and cleaning up each test, you can define the following method

  18. virtual void SetUp() {

  19. // additional set-up

  20. }

  21. virtual void TearDown() {

  22. // additional clear up

  23. }

  24. // objects declared here can be used by all tests in the test case for Foo.

  25. };

  26.  
  27. // Tests that the Foo::Bar() method does Abc.

  28. TEST_F(FooTest, MethodBarDoesAbc) {

  29. const string input_filepath = "xxx";

  30. const string output_filepath = "yyy";

  31. Foo f;

  32. EXPECT_EQ(0, f.Bar(input_filepath, output_filepath);

  33. }

  34.  
  35. TEST_F(FooTest, DoesXyz) {

  36. // Exercises the Xyz feature of Foo.

  37. }

  38.  
  39. int main(int argc, char**argv) {

  40. ::testing::InitGoogleTest(&argc, argv);

  41. return RUN_ALL_TESTS();

  42. }

::testing::InitGoogleTest()函数从命令行解析被Google Test识别的标记,删除所有识别的标记。这个函数允许用户通过不同的标记控制测试函数的行为。在高级指南中为详细介绍。在调用RUN_ALL_TESTS()之前必须调用这个函数,不然标记不会被正确的初始化。

在Windows上,InitGoogleTeset()函数兼容宽字符,所以可以在UNICODE mode的程序中使用。

但是,你可能想到写main()函数工作太多,我们完全统一,这就是Google Test提供一个基本的main()实现。如果这个符号你的需求,只需要将程序和gtest_main的库链接即可。出发吧~。

对Visual C++用户的重要备注

如果你把测试放入函数库里,而且你的主函数在不同的库里或者exe文件中,这些测试不会运行。原因是Visual C++的bug。当你定义测试时,Google Test建立一些静态对象来注册他们。这些对象不能在其它地方引用,但是他们的构造函数可以运行。Visual C++链接器看到这个库没有被其它地方一用,就把这个库给丢了。你必须在mian程序中引用你的带测试的库,以防止链接器丢弃它。下面是如何解决这个问题。在你的库代码的某个位置,写下如下代码:

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

如果你把测试放在静态的库里面(不是DLL),那么__declpsec(dllexport)不需要。现在,在你的主程序里,写一个代码,链接那个函数:

  1. int PullInMyLibrary();

  2. static int dummy = PullInMyLibrary();

这个会保证测试被引用,会使得他们在程序运行时注册

另外,如果你把测试定义在静态库里,在主程序里的链接程序中添加 /OPT:NOREF。如果你使用MSVC++ IDE,在.exe的工程properties/Configuration Properties/Linker/Optimization,设置引用设置为保留未被引用的数据(/OPT:NOREF)。这个会保证Visual C++链接器不会丢弃在最后可执行程序中产生的独立符号。

已知限制

Google Test是按照线程安全来设计的。在pthread库可用的系统上,它的实现是线程安全的。现在在其它系统上(例如Windows)利用两个线程并行运行Google Test的断言是不安全的。在大多数的测试中,这不是问题,只要断言是在主进程中完成。如果你想要帮助,你可以支援gtest-port.h中的在你的平台上的同步原语的必要实现。

发布了78 篇原创文章 · 获赞 17 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43778179/article/details/104923513
今日推荐