GoogleTest 入门
潘忠显 / 2021-12-15
本文是对 Googletest Primer 的阅读理解,对共享数据等功能做了额外的解释,并提供了示例代码协助理解这些功能如何使用。
一、背景
1.1 测试应具备的特点
- 独立、可重复
- 组织良好、反应被测试代码的结构
- 可移植、可复用
1.2 测试框架应具备的特点
-
支持测试应具备的上述特点
-
在出错时尽可能详细地显示信息
-
让测试人员专注于测试内容,而非框架
-
高效运行
其他关键词:
1.3 安装
二、概念
Test Program, Test Suite, Test, Assertion, Test Fixture
历史原因,googletest API 里边对 Test / Test Case / Test Suite 的使用和其他一些材料或者出版物上的同名概念有不同。目前正在将原来的 Test Case 修改成 Test Suite,而原来的 Test 含义太宽泛可以表示 Test Case,就没有更新。
2.1 概念之间的联系
-
写测试就从写断言开始,一个断言的结果有成功、非致命失败、致命失败
-
断言组成 Test,如果一次测试崩了,或者有失败的断言,这次测试就是失败的
-
Test 组成 Test Suite,通过这种组织结构反映被测试的代码结构
-
同一 Test Suite 之间的通过 Test Fixture 类来复用和共享公共对象数据和 subroutines
-
Test Suite 组成测试程序
三、实践
3.1 断言语句
-
ASSERT_*
对应致命错误 -
EXPECT_*
对应非致命错误
如果前面错误影响到后边的测试了才会用 ASSERT_*
,不然通常用 EXPECT_*
,因为一次测试能尽可能多的输出测试的问题。
ASSERT_*
失败,中断退出的是 Test,不是 Test Suite,也不是 Test Program;退出可能带来资源未清理,进而引起内存泄漏等其他问题,跟内存泄漏检测工具一起使用的时候注意一下就好。
可以在上边系列函数之后,使用 >>
, 通过流追加自定义的错误信息提示。只有失败的时候才会打印,类似 std::cout
的用法。
3.2 简单测试 - TEST()
TEST(TestSuiteName, TestName)
两个名字需要是有效的 C++ 符号,而且不能包含 _
。不同 Test Suite 中的 TestName 可以是相同的。
一个 Test 是通过 TEST() 来标定的,而一个 Test Suite 没有特别的符号划分范围,而是通过 TestSuiteName 相同的来进行组织。
TestSuiteName 和 TestName 的命名规范都遵循谷歌的 C++ 函数命名规范。
3.3 数据复用与共享 - TEST_F()
如果多个 Test 中使用类似的数据,可以利用 Test Fixture(可译作“测试配件”)。使用分为两部分:Fixture 的定义和 TEST_F()
编写。
Fixture 类的定义
-
独立定义,继承自
::testing::Test
,注意要是public
继承 -
命名需要按照规范,
XxxxTest
表示测用于测试Xxxx
的 -
所有的成员需要声明为
protected
以便访问 -
使用
SetUp()
成员函数作为构造函数,用于每次测试前的清理工作;带override
能够确保是需要覆盖的、正确的SetUp()
而不是其他函数 -
使用
TearDown()
作为析构函数,释放资源,用法类似于SetUp()
TEST_F()
的执行过程
使用 Fixture 需要用到 TEST_F(TestFixtureName, TestName)
以便访问到对象中的数据。每次调用 TEST_T 都会新创建一个 TestFixtureName 的对象,而不会重复利用,换句话说,不同 TEST_F 中的 Fixture 不会相互影响。
单个 TEST_F 的执行过程:
- 构造
TestFixture
对象 - 调用该对象的
SetUp()
进行数据的初始化 - 执行
TEST_F(){}
中的所有测试 - 调用对象的
TearDown()
进行数据的清理 - 销毁对象
复用初始数据
因为每次调用 TEST_F() 都单独创建 Fixture 并在结束时销毁会创建 Fixture 对象,所以普通的成员变量都会被重新初始化一遍,以此来可以来复用初始的数据。
Note that different tests in the same test suite have different test fixture objects, and googletest always deletes a test fixture before it creates the next one. googletest does not reuse the same test fixture for multiple tests. Any changes one test makes to the fixture do not affect other tests.
共享数据
Primer 里有句描述,提到了相同的 Test Suite 之间的 Test 可以通过 Fixture 共享数据,但之后的介绍只强调了每个 Test 会创建新的 Fixture,却没有提到如何共享数据:
When multiple tests in a test suite need to share common objects and subroutines, you can put them into a test fixture class.
尽管每次创建创建是新的对象,但是 C++ 的类支持 静态成员变量和静态成员函数,静态成员变量是类的所有对象共享的,静态成员函数则是可以操作静态成员变量的。利用这一点,便可以在不同的 Test 之间进行数据的共享了。
3.4 执行所有的测试
TEST()
和 TEST_F()
两个宏会隐式地向 googletest 注册所有的测试。
通常情况下,使用者只需要写 TEST 和 TEST_F 就可以,不用写 main()
函数,也不用主动调用 RUN_ALL_TESTS()
。具体的就是,直接链接 gtest_main
库就可以:
g++ test_google_test.cc -lgtest_main -lgtest -lpthread -o test_google_test
直接运行构建出的结果即可:
./test_google_test
3.5 个性化定制测试
有些情况,上边直接通过框架进行测试无法满足需求,就需要显式的创建 main()
函数,显式地调用 RUN_ALL_TESTS()
宏定义执行所有的测试。
关于 RUN_ALL_TESTS()
需要注意两点:
- 不能忽略
RUN_ALL_TESTS()
的结果。只要测试中有一处错误(致命或非致命)发生,其返回值就为1,所有的成功才为 0 - 只调用
RUN_ALL_TESTS()
一次
在调用 RUN_ALL_TESTS() 之前,须先调用一下 ::testing::InitGoogleTest()
来从命令行解析 googletest 的 flag、删除已识别的flag(?)、使用者自己传入特定的 flag 等等。
最简单的 main 函数如下,这样就不用链 gtest_main 库了:
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
四、覆盖上述要点的示例
// test_google_test.cc
#include "gtest/gtest.h"
TEST(TestSuite1, Assert) {
ASSERT_TRUE(false); // 要点1: 中断该Test
EXPECT_EQ(1, 1);
}
TEST(TestSuite1, Equal) {
EXPECT_TRUE(false); // 要点2: 继续后边语句,但该Test失败
EXPECT_EQ("463ac35c9f6413ad48485-01", "463ac35c9f6413ad48485-01");
}
class FixtureTest : public testing::Test {
protected:
void SetUp() override {}
void TearDown() override {}
int val_to_reuse{1}; // 要点3: 每个TEST_F重新创建
static int val_to_share; // 要点4: 相同Test Suite可以传递值
};
int FixtureTest::val_to_share = 1;
TEST_F(FixtureTest, Test1) {
val_to_reuse++;
val_to_share++;
EXPECT_EQ(val_to_reuse, 2);
EXPECT_EQ(val_to_share, 2);
}
TEST_F(FixtureTest, Test2) {
val_to_reuse++;
val_to_share++;
EXPECT_EQ(val_to_reuse, 2);
EXPECT_EQ(val_to_share, 3);
}
死亡测试
死亡断言
如果断言失败,没有继续进行后续所有测试的必要,尤其可能会引发严重内存损坏、安全漏洞等问题时,我们应当选择使用死亡测试——使用死亡断言:
-
statement
非零且stderr
与matcher
匹配-
EXPECT_DEATH(statement, matcher)
-
ASSERT_DEATH(statement, matcher)
-
-
仅在支持死亡测试的条件下等价于上边两句(什么情况下会不支持)
-
EXPECT_DEATH_IF_SUPPORTED(statement, matcher)
-
ASSERT_DEATH_IF_SUPPORTED(statement, matcher)
-
-
仅在支持死亡测试的条件下生效
-
EXPECT_DEBUG_DEATH(statement, matcher)
-
ASSERT_DEBUG_DEATH(statement, matcher)
-
-
使用
predicate
来指定推出时的状态码EXPECT_EXIT(statement, predicate, matcher)
ASSERT_EXIT(statement, predicate, matcher)
命名规范
- 当 Test Suite 包含死亡测试时,命名应当以
DeathTest
结尾,如TEST(MyDeathTest, NormalExit)
,以DeathTest
结尾的Test Suite 会在其他测试之前运行 - 如果 Test Fixture 同时被普通测试和死亡测试使用,使用
using FooDeathTest = FooTest;
区分,避免代码重复
death_test_style
短线成环境下,使用 threadsafe
两种设置方式:
- 代码中显式调用
GTEST_FLAG_SET(death_test_style, "threadsafe")
- 命令行使用
--gtest_death_test_style=threadsafe
更复杂的断言
更多测试内容
测试类型
参数化类型测试
私有变量
更多的编码方式
通过处理测试事件扩展 googletest
以编程方式注册测试
流程控制
跳过
类似的测试框架都会提供提过的指令,googletest中也有GTEST_SKIP()
。
-
在 TestCase 中使用
GTEST_SKIP()
跳过单个测试用例 -
在 TestFixture 的
SetUp()
中使用GTEST_SKIP()
跳过所有该 Fixture 的TEST_F()
异常
工具使用
正则表达式
多线程
多进程
RecordProperty(“key”, value)
信息展示与记录
打印变量
在 EXPECT_EQ 等断言失败的时候,googletest 会将预期值和实际值都打印出来。如果是自定义的类型,该如何让 googletest 实现打印?有四种方式:
-
在成员函数中声明友元函数
friend std::ostream& operator<<(std::ostream& os, const Bar& bar)
可以将公私成员进行打印 -
在成员函数外声明普通函数
std::ostream& operator<<(std::ostream& os, const Bar& bar)
,可以调用其公有成员进行打印 -
类内定义友元函数
PrintTo()
代替第一种方式重载<<
进行打印 -
类外定义函数
PrintTo()
代替第二种<<
进行打印
前两种方式不能同时定义,会有编译错误;后两种也是不能同时定义。如果同时定义了 <<
和 PrintTo()
,googletest会选择使用 PrintTo()
。
获取当前测试的名称
生成测试报告
XML
JSON
更多的选项
命令行选项
快速测试是快速迭代的基础
测试代码是业务的代码的第一个用户,能发现很多问题
一套糟糕的测试集,还不如根本就别测试
测试绝不能是出问题时的补救措施
绝不能把产品可靠性寄托在程序员自身能力上(can’t rely on programmer ability alone to avoid product detects)
编写测试代码和编写优秀有效的测试代码是两码事
测试代码作为功能文档,前提条件是测试代码本身必须精简
越来越多的程序员开始编写越来越小的测试,反而带来了更稳定更快速的结果。
无论哪种测试“规模”都包括了两个的属性:大小和范围(size and scope)
Size(大小)意味着运行测试时所需要的资源,包括内存、进程数量、运行时间等;Scope(范围)则是测试中验证的代码路径(code path)。
- 小型测试其他的限制包括不能sleep,不能做I/O操作,也不能执行任何阻塞式操作
- 中型测试可以启动多个不同进程,使用多个线程,并且可以调用阻塞接口,包括对且仅对localhost的访问等等
- 大型测试移除掉了对localhost访问的限制,允许测试和需要测试的系统能够通过网络访问多台机器,例如说测试可以访问远程集群中的系统
我们完全可以通过少量测试就执行大量的代码,但我们压根不去检查没行代码是不是真的做了什么有效工作。代码覆盖率仅仅检查一行代码是否被执行,而不是执行后的结果如何。
如果指望用一个简单的数字来回答“我们的测试是否足够”这个问题,就未免过于轻率了。代码覆盖率能够用来对未能测试到的代码提供一些数据参考,但是它并不能代替使用批判性思维来评估你的系统是否充分测试了。
依靠展示成功的项目实践,可能更有利于传播这种理念
如果刷过leetcode这些,就知道那些test case其实都是单测
要把测试代码写好,具备真正可维护性并能真正帮我们提高生产效率,必须防治brittle(脆弱的)和unclear(不清晰)的测试代码
WordList
- predicate 谓词、宾语
- mitigate 缓解、降低
参考文档
Google的测试思路 https://km.woa.com/articles/show/469520
Google单元测试指导原则 https://km.woa.com/articles/show/470514