Google Test教程

动机

以前写了自用的基础库,为了保证基础库的质量,用过Google Test库写了一点单元测试。当时通过单元测试发现了很多忽略的问题,给我的震撼很大。从此就觉得不写单元测试就对自己的代码放心不下。

Chromium项目中有着大量的单元测试,虽然我写了这么久的代码,却从没写过Chromium的单元测试。另外觉得自己对单元测试的理解还不够透彻,所以现在在回过头重新学习一下Google Test,把这个库的使用总结一遍。

编译Google Test库

Windows上编译Google Test库使用CMake比较简单,这也是官方教程所推荐的。打开CMake,定位到Google Test项目所在的代码目录,如下:

cmake_google_test

勾选上“Build_ShARED_LIBS”就可以编译出Dll动态库文件了,否则就是Lib静态库。

使用Google Test库

使用Google Test库有2种方式,以下一一介绍。

源代码方式使用Google Test库

这种方式是把Google Test库的头文件和源代码直接加入到单元测试工程中,然后一起编译。

  • 优点。直接管理代码,不依赖其他库。
  • 缺点。步骤繁琐。多个单元测试工程的重复代码太多。

这种管理方式项目集成Google Test库的代码比较麻烦,不建议这样做。

使用Google Test静态库或者动态库

这种方式是编译Google Test库为一个库。使用的时候工程里直接加上库的头文件和库文件就可以了,比较方便。通过CMake生成sln的编译工程,打开sln工程然后选择生成MT、MD的debug版和release版,单元测试项目就可以方便的使用了。

Main函数

以库的方式使用Google Test,可以不提供Main函数的实现。也就是说,仅仅写单元测试的代码就足够了。

比如以静态库方式使用Google Test。链接上gtest.lib和gtest_main.lib,后面只需要写相关的单元测试代码了。因为gtest_main.lib库里面已经提供了一个默认的main函数初始化Google Test实现,如下:

我个人觉得还是自己实现一个main函数比较好,一是工作量不大,自己可以自由控制。二是减少依赖gtest_main.lib库。

::testing::InitGoogleTest函数解析命令行,以便Google Test使用不同的选项,这允许通过命令行来控制单元测试程序的一些行为。

在Windows中,::testing::InitGoogleTest也可以使用宽字符串,所以单元测试程序也可以很方便的编译成UNICODE版本。

介绍Google Test库

Google Test库的Url是:https://github.com/google/googletest 。上面有项目的代码和详细的文档。

好的单元测试库的特性

  1. 测试的独立性和可重复性。一个测试的结果不应该影响到其他的测试。Google Test框架把每个测试独立在不同的对象中。当一个测试失败了,Google Test框架允许独立的运行进行快速调试。
  2. 测试应该被很好的组织起来用于反映被测试代码的结构。Google Test框架组织相关的测试到一个测试用例,这样就可以共享数据和一些函数。
  3. 测试应该可以移植和可重用。Google Test框架支持许多不同的系统和编译器。
  4. 当测试失败了,应该提供足够多的定位信息。当一个测试失败了,Google Test框架不会立即停止,它只会停止当前的测试然后继续下面的测试。
  5. 测试框架应该简单,不要增加使用者的负担。
  6. 加使用者的负担。

简单的单元测试项目框架

下面以一个简单的项目为例子,介绍如何搭建一个基于Google Test的单元测试项目。这项目用的是Google Test静态库,自己实现一个Main函数来初始化Google Test库。

假如我们需要测试以下代码,头文件和源文件分别为calc.h和calc.cpp。以下是两个文件的内容。

calc.h:

然后我们建立一个名为Unittest_Demo的控制台工程。配置项目把Google Test库的头文件目录添加到项目的附加目录。如下是Main函数相关代码:

第三行代码是include Google Test库的头文件,第四行是链接Google Test库。main函数里面第一行是初始化Google Test库,第二行是运行所有的测试。

然后我们添加一个calc_unittest.cpp文件,就是对calc.cpp代码的单元测试,其内容如下:

如上,使用TEST()宏来定义一个测试函数,如下形式:

TEST宏第一个参数是测试用例名,第二个参数是测试名。这个宏函数没有返回值。我们在宏函数体里面可以写一些测试代码。

值得注意的是:test_case_name和test_name必须是合法的c++标识符,并且不包含下划线_。因为TEST宏会用_来连接扩展test_case_name和test_name。这两个标示符本身带有_会导致混淆。

Google Test库根据测试用例名来组织用例,所以逻辑上相关的测试应该使用相同的测试用例名。最终这个单元测试顺利通过,其输出的结果如下:

google_test_result

断言

Google Test单元测试库里面最重要就是断言,断言是通过一系列断言宏实现的。断言宏就好像一个函数,它检查一个语句的条件是true还是false。当一个断言失败了,Google Test就会打印断言所在的源代码文件名,行号还有一些断言信息。你可以自定义一些断言信息。

这里断言宏有着不同的效果。ASSERT_*这类断言宏失败时会生成致命失败的信息,然后终止当前的测试函数。EXPECT_*这类断言宏失败时会生成非致命失败的信息,不会终止当前的测试函数。通常EXPECT_*是更好的选择,因为它允许我们在一个测试中报告多个失败的测试结果。如果是一个非常严重的错误,使用ASSERT_*也是很有理由的。

通常ASSERT_*失败了就会从测试函数中立即返回,可能会跳过测试函数后面的清理代码,这会导致一些资源泄露。所以小心这里问题会影响到测试结果的准确性。

如果想增加自定义的失败消息,可以用<<操作法来实现,如下面例子:

这种用法跟c++标准库里面的ostream类似。

基本断言

这类断言只是简单测试true/false值。

致命断言 非致命断言 通过条件
ASSERT_TRUE(condition) EXPECT_TRUE(condition) true
ASSERT_FALSE(condition) EXPECT_FALSE(condition) false

当ASSERT_*失败之后,就不会再执行这个测试后面的断言了。而EXPECT_*失败之后,还会继续执行这个测试后面的断言。一个测试失败,并影响执行后面的测试。

值断言

这类断言比较两个值。

致命断言 非致命断言 通过条件
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

当这些断言失败的时候,Google Test会打印出val1和val2的值。

在ASSERT_EQ*和EXPECT_EQ*中,你第一参数应该是期望值,第二个参数应该是实际值。

这些断言宏也可以用来比较自定义类型,只要这个类型支持比较操作符。

对于指针来说,这些断言宏是比较指针本身的值,而不是比较指向的对象。所以对于c风格的字符串,不能用这些断言宏来比较,应该使用接下来介绍的字符串断言宏。

显然,这些断言宏是支持string和wstring的。

字符串断言

这类用来比较两个c类型的字符串。如果想要比较两个string对象,请用EXPECT_EQ、EXPECT_NE来代替。

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

这些字符串宏也可以比较同样类型的窄和宽字符。NULL和空字符串是不相等的。

更多高级断言

Google Test还有一些其他用的比较少的高级断言宏,将在这个章节里一一介绍。

显式成功或者失败断言

这类宏不实际的测试值或者表达式。仅仅是生成一个成功或者失败。

断言宏 介绍
SUCCEED() 生成一个成功。在同个测试中,如果后面有失败的断言,则这个测试还是失败。其实这个成功断言对于用户来说几乎不可见,目前还没有什么实际意义
FAIL() 生成一个致命的错误
ADD_FAILURE() 生成一个非致命错误。
ADD_FAILURE_AT(“file_path”,line_number) 在指定文件的指定行生成一个非致命错误

异常断言

这些断言用于测试一段代码是否会抛出异常。

致命断言 非致命断言 通过条件
ASSERT_THROW(statement, exception_type) EXPECT_THROW(statement, exception_type) 抛出了指定类型的异常
ASSERT_ANY_THROW(statement) EXPECT_ANY_THROW(statement) 抛出了任何异常
ASSERT_NO_THROW(statement) EXPECT_NO_THROW(statement) 不抛出任何异常

下面是一段代码例子:

浮点断言

因为浮点值有精度误差的问题,所以一般不能直接判断两个浮点值是否相等。Google Test特意准备了一组处理浮点值的断言宏。

致命断言 非致命断言 通过条件
ASSERT_FLOAT_EQ(expected, actual) EXPECT_FLOAT_EQ(expected, actual) 两个float类型值相等
ASSERT_DOUBLE_EQ(expected, actual) EXPECT_DOUBLE_EQ(expected, actual) 两个double类型值相等
ASSERT_NEAR(val1, val2, abs_error) EXPECT_NEAR(val1, val2, abs_error) 在指定误差内相等

HRESULT断言

这是为Windows函数返回HRESULT类型值准备的断言宏。失败的时候,断言宏还是解释HRESULT值的错误码意义。

致命断言 非致命断言 通过条件
ASSERT_HRESULT_SUCCEEDED(expression) EXPECT_HRESULT_SUCCEEDED(expression) HRESULT为成功
ASSERT_HRESULT_FAILED(expression) EXPECT_HRESULT_FAILED(expression) HRESULT为失败

测试

一个TEST宏就是生成一个测试,一个测试里面可以包含多个断言。假如一个测试用例对应一个被测试的函数,那么一个测试就是相当于更小一层次的测试情况,比如测试参数、测试返回值、测试各种异常情况。

TEST宏最终会展开成以下代码,宏展开之后的代码比较复杂,其实我们也不必过多关注:

调用测试

TEST和TEST_F(测试夹具中使用)都会因此的向Google Test中注册测试,这是很方便的地方。

代码调用RUN_ALL_TESTS函数来运行所有测试,如果返回0,代表所有测试都运行通过。当我们调用RUN_ALL_TESTS:

  1. 保存Google Test的所有状态。
  2. 为第一个测试创建一个测试夹具。
  3. 通过SetUp初始化。
  4. 通过TearDown来清理。
  5. 删除夹具。
  6. 恢复所有状态。
  7. 对下一个测试重复以上步骤,直到所有测试都执行完毕。

我们只需要调用一次RUN_ALL_TESTS,一般来说就在main函数里面调用一次。

测试夹具(Test Fixture)

如果你发现多个测试处理相同的数据,你就可以使用测试夹具。测试夹具可以让多个不同的测试使用相同的环境。比如你需要测试一个Queue类,许多个测试都要操作多个Queue对象。你可以定义一个测试夹具来准备一下相同的数据,稍后TEST_F定义的测试就可以访问和复用这些数据。

从使用上来看TEST_F跟TEST定义从测试没有区别,只是TEST_F的测试可以访问测试夹具中的数据和函数。

创建测试夹具

创建一个测试夹具的步骤如下:

  1. 以protected或者public的方式从::testing::Test派生出一个类。
  2. 在类里面,准备一些你在测试中使用的对象。
  3. 如果需要,你可以写一个默认构造函数或者SetUp函数,用来给每个测试准备对象。注意SetUp的函数名。
  4. 如果需要,写一个析构函数或者TearDown函数用来释放你在SetUp里面分配的资源。
  5. 如果需要,为测试定义共享的程序。

TEST_F

以下是TEST_F定义一个测试:

TEST_F的第一个参数test_case_name必须是测试夹具的类名。在使用TEST_F之前,必须定义好测试夹具。

对于每个用TEST_F定义的测试来说:

  1. 运行时每次都创建一个新的夹具。
  2. 直接通过SetUp初始化。
  3. 执行测试。
  4. 调用TearDown清理。
  5. 删除测试夹具。

同一个测试用例里面的不同测试是使用独立的测试夹具数据。所以不同测试之间不会互相影响。

测试夹具例子

以下以测试一个Queue类来介绍测试夹具的使用。先定义一个Queue类。

然后定义夹具类QueueTest。通常测试Foo类,则定义一个FooTest的夹具。

上面代码中TearDown不需要做任何清理工作,如果有分配资源,别的地方没有自动释放,则需要再TearDown里面去释放。

以上是定义两个测试,使用测试夹具中的数据。

实例

Google Test库的samples目录中有一些例子,如果想更进一步的学习如何使用Google Test来开展单元测试,看这些例子是比较好。

测试夹具复用

在第5个例子中通过从一个测试夹具派生出另外一个测试夹具来达到复用的目的。代码如下:

另外,从例子中看出,在测试夹具中也可以使用断言。其实在其他任何c++代码中都可以使用断言。

全局SetUp和TearDown

你可以定义测试或者测试用例级别的SetUp和TearDown,也可以定义全局级别的设置。定义全局的设置,你必须从::testing::Environment代码如下:

我们自己派生出一个FooEnvironment:

然后main函数中在Google Test初始化之后,RUN_ALL_TESTS之前注册我们的FooEnvironment:

当RUN_ALL_TESTS调用时,会先调用我们的SetUp,然后最后调用我们的TearDown。我们可以注册多个Environment,执行时按照注册的顺序调用。

测试私有代码

对于一些匿名或者静态的函数,没有声明,只有实现,最好重构一下代码,为单元测试提供一个测试的头文件接口。

对于类的私有部分,可以用FRIEND_TEST(TestCaseName, TestName);宏来声明某个测试为友元,这样测试就可以访问类的私有部分。如下:

单元测试选项

Google Test可以通过命令行参数来控制单元测试测试的行为。

列出所有的单元测试

通过传递–gtest_list_tests命令行参数来打印出所有的单元测试用例和测试,输出结果如下:

这样,单元测试程序只会列出名字而不会执行测试。

运行部分测试

默认的Google Test运行所有定义的测试,我们可以通过–gtest_filter参数来指定运行一些测试。例如:

  1. –gtest_filter=FooTest.*。只运行FooTest测试用例。
  2. –gtest_filter=*Null*:*Constructor*。只运行名为Null或者为Constructor的测试。
  3. –gtest_filter=FooTest.*-FooTest.Bar。运行FooTest测试用例里面的所有测试,除了Bar测试。

XML报告

可以使用–gtest_output来输出一个测试报告,如下:

这样运行单元测试,结果会以xml格式输出到当前目录的a.xml文件中。

发表评论

电子邮件地址不会被公开。 必填项已用*标注