Jason Pan

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 概念之间的联系

三、实践

3.1 断言语句

如果前面错误影响到后边的测试了才会用 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 类的定义

TEST_F() 的执行过程

使用 Fixture 需要用到 TEST_F(TestFixtureName, TestName) 以便访问到对象中的数据。每次调用 TEST_T 都会新创建一个 TestFixtureName 的对象,而不会重复利用,换句话说,不同 TEST_F 中的 Fixture 不会相互影响。

单个 TEST_F 的执行过程:

复用初始数据

因为每次调用 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() 之前,须先调用一下 ::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);
}

死亡测试

死亡断言

如果断言失败,没有继续进行后续所有测试的必要,尤其可能会引发严重内存损坏、安全漏洞等问题时,我们应当选择使用死亡测试——使用死亡断言:

命名规范

death_test_style

短线成环境下,使用 threadsafe

两种设置方式:

更复杂的断言

更多测试内容

测试类型

参数化类型测试

私有变量

更多的编码方式

通过处理测试事件扩展 googletest

以编程方式注册测试

流程控制

跳过

类似的测试框架都会提供提过的指令,googletest中也有GTEST_SKIP()

异常

工具使用

正则表达式

多线程

多进程

RecordProperty(“key”, value)

信息展示与记录

打印变量

在 EXPECT_EQ 等断言失败的时候,googletest 会将预期值和实际值都打印出来。如果是自定义的类型,该如何让 googletest 实现打印?有四种方式:

前两种方式不能同时定义,会有编译错误;后两种也是不能同时定义。如果同时定义了 <<PrintTo(),googletest会选择使用 PrintTo()

获取当前测试的名称

生成测试报告

XML

JSON

更多的选项

命令行选项

快速测试是快速迭代的基础

测试代码是业务的代码的第一个用户,能发现很多问题

一套糟糕的测试集,还不如根本就别测试

测试绝不能是出问题时的补救措施

绝不能把产品可靠性寄托在程序员自身能力上(can’t rely on programmer ability alone to avoid product detects)

编写测试代码和编写优秀有效的测试代码是两码事

测试代码作为功能文档,前提条件是测试代码本身必须精简

越来越多的程序员开始编写越来越小的测试,反而带来了更稳定更快速的结果。

无论哪种测试“规模”都包括了两个的属性:大小和范围(size and scope)

Size(大小)意味着运行测试时所需要的资源,包括内存、进程数量、运行时间等;Scope(范围)则是测试中验证的代码路径(code path)。

我们完全可以通过少量测试就执行大量的代码,但我们压根不去检查没行代码是不是真的做了什么有效工作。代码覆盖率仅仅检查一行代码是否被执行,而不是执行后的结果如何。

如果指望用一个简单的数字来回答“我们的测试是否足够”这个问题,就未免过于轻率了。代码覆盖率能够用来对未能测试到的代码提供一些数据参考,但是它并不能代替使用批判性思维来评估你的系统是否充分测试了。

依靠展示成功的项目实践,可能更有利于传播这种理念

如果刷过leetcode这些,就知道那些test case其实都是单测

要把测试代码写好,具备真正可维护性并能真正帮我们提高生产效率,必须防治brittle(脆弱的)和unclear(不清晰)的测试代码

WordList

参考文档

Google的测试思路 https://km.woa.com/articles/show/469520

Google单元测试指导原则 https://km.woa.com/articles/show/470514