0%

GoogleTest 快速入门

  • 在项目中使用 GTest 编写单测以保证代码质量是必要的,通过构造各种单元测试的case,可以测试功能是否正确,发现肉眼不容易发现的bug。
  • GTest 是 Google 发布的一款非常优秀的开源 C/C++ 单元测试框架,它简单易用,功能完善,已被应用于多个开源项目及Google内部项目中:ChromeWeb浏览器LLVM编译器ProtocolBuffers数据交换格式等。
  • 本文分享 GTest 的核心用法、常用技巧以及单测编写的思路,包含最必要、最常用的内容,能覆盖大部分场景的单测需求,同时会引用外部文档供扩展阅读。

安装

官方传送门:GoogleTest - Google Testing and Mocking Framework
现在官方已经把 GoogleTest 和 GoogleMock 一起维护,所以这个 git 仓库还包含了 GoogleMock。

GitHub 源码仓库安装

1
2
3
4
5
6
7
➜  ~ wget https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz
➜ ~ tar xf v1.14.0.tar.gz
➜ ~ cd googletest-1.14.0
➜ googletest-1.14.0 cmake -DBUILD_SHARED_LIBS=ON .
➜ googletest-1.14.0 make
➜ googletest-1.14.0 sudo cp -a googletest/include /usr/include
➜ googletest-1.14.0 sudo cp -a lib/libgtest_main.so lib/libgtest.so /usr/lib/

这样就 OK 了,可以用 sudo ldconfig -v | grep gtest 检查,看到下面就 OK 了:

1
2
libgtest.so -> libgtest.so
libgtest_main.so -> libgtest_main.so

Ubuntu apt 源码安装

1
2
3
4
5
➜  ~ sudo apt-get install libgtest-dev
➜ ~ cd /usr/src/gtest
➜ gtest sudo cmake CMakeLists.txt
➜ gtest sudo make
➜ gtest sudo cp *.a /usr/lib

上述步骤OK后就可以使用-lgtest选项编译测试代码了。

概念

Test Suite

1
2
3
4
TEST(TestSuiteName, TestCaseName) {
// 单测代码
EXPECT_EQ(func(0), 0);
}
  • TestSuiteName 用来汇总 test case,相关的 test case 应该是相同的 TestSuiteName。一个文件里只能有一个 TestSuiteName,建议命名为这个文件测试的类名。
  • TestCaseName 是测试用例的名称。建议有意义,比如“被测试的函数名称”,或者被测试的函数名的不同输入的情况。
  • TestSuiteName_TestCaseName 的组合应该是唯一的。
  • GTest 生成的类名是带下划线的,所以上面这些名字里不建议有下划线。

Test Case

一个 TEST(Foo, Bar){...} 就是一个 Test Case。考虑到构造输入有成本,通常一个 TEST(Foo, Bar) 里会反复修改输入,构造多个 case,测试不同的执行流程。这里建议用大括号分隔不同的 case,整体更条理。另一个好处在于:每个变量的生命周期仅限于大括号内。这样就可以反复使用相同的变量名,而不用给变量名编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TEST(Foo, bar) {
// case 1: enable = true
{
Context ctx;
params.enable_refresh = true;
ASSERT_EQ(ctx->is_enable_fresh(), true);
}

// case 2: enable = false
{
Context ctx;
params.enable_refresh = false;
ASSERT_EQ(ctx->is_enable_fresh(), false);
}
}

如果待测函数十分复杂,建议拆分多个 TEST(Foo, Bar){...},避免 Test Case 代码膨胀。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 待测函数
int foo(TarAddr ta) {
if (!ta)
return -1;
switch(ta.did) {
case 0x193b:
...
case 0x1204:
...
}
}

TEST(Foo, IsNil) {
...
}

TEST(Foo, 0x193b) {
...
}

TEST(Foo, 0x1204) {
...
}

使用

官方 WIKI:GTest

断言

GTest 使用一系列断言的宏来检查返回值是否符合预期、是否抛出预期的异常等。主要分为两类:ASSERTEXPECT。区别在于 ASSERT 不通过的时候会认为是一个 fatal 的错误,退出当前函数(只是函数)。而 EXPECT 失败的话会继续运行当前函数,所以对于函数内几个失败可以同时报告出来。

通常我们用 EXPECT 级别的断言就好,除非你认为当前检查点失败后函数的后续检查没有意义:

  • 如果某个判断不通过时,会影响后续步骤,要使用 ASSERT。常见的是空指针,或者数组访问越界。
  • 其他情况,可以使用 EXPECT,尽可能多测试几个用例。

基础的断言

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

数值比较

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

浮点数比较

Fatal assertion Nonfatal assertion Verifies
ASSERT_FLOAT_EQ(_val1_, _val2_); EXPECT_FLOAT_EQ(_val1_, _val2_); val1 == val2 (within 4 ULPs)
ASSERT_DOUBLE_EQ(_val1_, _val2_); EXPECT_DOUBLE_EQ(_val1_, _val2_); val1 == val2 (within 4 ULPs)
ASSERT_NEAR(_val1_, _val2_, _abs_error_); EXPECT_NEAR(_val1_, _val2_, _abs_error_); 判断两个数字的绝对值相差是否小于等于 abs_val

字符串比较

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

示例

创建一个简单的C++类,例如MyClass:

1
2
3
4
5
6
7
8
9
10
11
// MyClass.h
class MyClass {
public:
int Add(int a, int b) {
return a + b;
}

int Subtract(int a, int b) {
return a - b;
}
};

创建一个测试文件,例如MyClassTest.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// MyClassTest.cpp
#include "MyClass.h"
#include <gtest/gtest.h>

TEST(MyClassTest, AddTest) {
MyClass my_class;
EXPECT_EQ(my_class.Add(1, 2), 3);
EXPECT_EQ(my_class.Add(1, 1), 3);
}

TEST(MyClassTest, SubtractTest) {
MyClass my_class;
EXPECT_EQ(my_class.Subtract(1, 2), -1);
EXPECT_EQ(my_class.Subtract(1, 1), 3);
}

int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

编译运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
➜  cd demo
➜ demo g++ -std=c++11 -pthread MyClassTest.cpp -lgtest -o MyClassTest
➜ demo ./MyClassTest
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from MyClassTest
[ RUN ] MyClassTest.AddTest
MyClassTest.cpp:7: Failure
Expected equality of these values:
my_class.Add(1, 1)
Which is: 2
3
[ FAILED ] MyClassTest.AddTest (0 ms)
[ RUN ] MyClassTest.SubtractTest
MyClassTest.cpp:13: Failure
Expected equality of these values:
my_class.Subtract(1, 1)
Which is: 0
3
[ FAILED ] MyClassTest.SubtractTest (0 ms)
[----------] 2 tests from MyClassTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 2 tests, listed below:
[ FAILED ] MyClassTest.AddTest
[ FAILED ] MyClassTest.SubtractTest

2 FAILED TESTS

编写规范

💡 单测代码也需要经过 Code Review。单测代码和线上代码同等重要。

目录结构、文件与命名规范

单测的目录结构,要和源码的目录结构一致

单测文件的路径名,等价于源码的文件名加上 _test 后缀。

目的在于:让写单测的人能很快定位是否已经有这个文件或这个类的单测,让新增代码更聚合,避免写重复单测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// bad
src/
common/
item_data.cpp
frame/
request_context.cpp
unittest/
item_data_test.cpp
request_context_test.cpp

// good
src/
common/
item_data.cpp
frame/
request_context.cpp
unittest/
common/
item_data_test.cpp
frame/
request_context_test.cpp

TestSuite、TestCase 命名规范

TestSuite 建议命名为被测试的类名加上 Test 后缀:

1
2
3
4
5
// bad
TEST(MyTest, foo) {...}

// good
TEST(RequestContextTest, foo) {...}

TestCase 建议命名为被测试的函数名,不要随意起名,也不需要增加不必要的前缀:

1
2
3
4
5
6
7
8
9
// bad
TEST(RequestContextTest, test_uav) {
ASSERT_EQ(ctx->init_uav_to_group_bid(), 1);
}

// good
TEST(RequestContextTest, init_uav_to_group_bid) {
ASSERT_EQ(ctx->init_uav_to_group_bid(), 1);
}

GTest 生成的类名是带下划线的,所以上面这些名字建议用驼峰形式。

单元测试代码编写规范

不要用 std::cout 输出变量值,改为用 ASSERT / EXPECT 检查

1
2
3
4
5
6
7
// bad 
std::cout << "ads_size = " << rsp.ads.size() << std::endl;
EXPECT_EQ(rsp.ads.size(), 1);

// good
EXPECT_EQ(rsp.size(), 1); // 这一行在检测失败时,会打印 rsp.size() 的值
EXPECT_EQ(rsp.size(), 1) << rsp.ads.debug_string() << std::endl; // 可以在检测失败时,打印更多 debug 日志

不要直接写数值,要写清楚这个数字是怎么算的

1
2
3
4
5
6
7
8
9
10
11
12
// bad
params.alpha = 2;
params.beta = 2.5;
ASSERT_EQ(params.get_score(), 2965); // 这 2965 咋算的?

// good
params.alpha = 2;
params.beta = 2.5;
ASSERT_EQ(params.get_score(), 2 * 2.5 * 593); // alpha * beta * ctx.bid

// good: 把变量名直接注释在字面量后面
ASSERT_EQ(params.get_score(), 2 /* alpha */ * 2.5 /* beta */ * 593 /* ctx.bid */);

使用大括号分隔、缩进不同的 Test Case

一个 TEST(Foo, Bar){...} 就是一个 Test Case。考虑到构造输入有成本,通常一个 TEST(Foo, Bar) 里会反复修改输入,构造多个 case,测试不同的执行流程。这里建议用大括号分隔不同的 case,整体更条理。另一个好处在于:每个变量的生命周期仅限于大括号内。这样就可以反复使用相同的变量名,而不用给变量名编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// bad
TEST(Foo, bar) {
Context ctx1;
params.enable_refresh = true;
ASSERT_EQ(ctx1->is_enable_fresh(), true);

Context ctx2;
params.enable_refresh = false;
ASSERT_EQ(ctx2->is_enable_fresh(), false);
}

// good
TEST(Foo, bar) {
// case 1: enable = true
{
Context ctx;
params.enable_refresh = true;
ASSERT_EQ(ctx->is_enable_fresh(), true);
}

// case 2: enable = false
{
Context ctx;
params.enable_refresh = false;
ASSERT_EQ(ctx->is_enable_fresh(), false);
}
}

为单测补充详细的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
req.type = Type::foo;
req.from = "localhost";
EXPECT_EQ(ctx.get_value(), 5);

// good:补充注释
req.type = Type::foo; // is_foo()
req.from = "localhost"; // is_local_req()
EXPECT_EQ(ctx.get_value(), 5); // 本地请求,默认值是 5

// best:代码即注释
req.type = Type::foo;
ASSERT_TRUE(ctx->is_foo());
req.from = "localhost";
ASSERT_TRUE(ctx->is_local_req());
EXPECT_EQ(ctx.get_value(), 5); // 本地请求,默认值是 5

参考文档

GTest 官方手册 (Google Test Primer)
C++ 研发基本功 - GTest / GMock 单元测试实践手册