在项目中使用 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) { { Context ctx; params.enable_refresh = true ; ASSERT_EQ (ctx->is_enable_fresh (), true ); } { 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 使用一系列断言 的宏来检查返回值是否符合预期、是否抛出预期的异常等。主要分为两类:ASSERT
和 EXPECT
。区别在于 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 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 #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 src/ common/ item_data.cpp frame/ request_context.cpp unittest/ item_data_test.cpp request_context_test.cpp 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 TEST (MyTest, foo) {...}TEST (RequestContextTest, foo) {...}
TestCase 建议命名为被测试的函数名,不要随意起名,也不需要增加不必要的前缀:
1 2 3 4 5 6 7 8 9 TEST(RequestContextTest, test_uav) { ASSERT_EQ(ctx->init_uav_to_group_bid(), 1 ); } 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 std::cout << "ads_size = " << rsp.ads.size () << std::endl; EXPECT_EQ (rsp.ads.size (), 1 );EXPECT_EQ (rsp.size (), 1 ); EXPECT_EQ (rsp.size (), 1 ) << rsp.ads.debug_string () << std::endl;
不要直接写数值,要写清楚这个数字是怎么算的 1 2 3 4 5 6 7 8 9 10 11 12 params.alpha = 2 ; params.beta = 2.5 ; ASSERT_EQ (params.get_score (), 2965 ); params.alpha = 2 ; params.beta = 2.5 ; ASSERT_EQ (params.get_score (), 2 * 2.5 * 593 ); ASSERT_EQ (params.get_score (), 2 * 2.5 * 593 );
使用大括号分隔、缩进不同的 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 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 ); } TEST (Foo, bar) { { Context ctx; params.enable_refresh = true ; ASSERT_EQ (ctx->is_enable_fresh (), true ); } { 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 req.type = Type::foo; req.from = "localhost" ; EXPECT_EQ (ctx.get_value (), 5 );req.type = Type::foo; req.from = "localhost" ; EXPECT_EQ (ctx.get_value (), 5 ); req.type = Type::foo; ASSERT_TRUE (ctx->is_foo ());req.from = "localhost" ; ASSERT_TRUE (ctx->is_local_req ());EXPECT_EQ (ctx.get_value (), 5 );
参考文档 GTest 官方手册 (Google Test Primer) C++ 研发基本功 - GTest / GMock 单元测试实践手册