准备工作
参考链接:研发基本功 -
GTest / GMock 单元测试实践手册
CMake 找到 GTest
先编译安装 GTest,下面是示例 CMakeLists.txt 内容如下:
1 2 3 4 5 find_package (GTest REQUIRED) // 找到GTestinclude_directories (${GTEST_INCLUDE_DIRS} )enable_testing () // 启用测试支持add_subdirectory (src) // 源码add_subdirectory (test ) // 添加 test 子目录,这下面就可以用 GTest 语法写测试函数
如何把写好的测试程序进行测试
我们已经在 test 文件夹下 SplitChineseTest.cpp
中写好一个测试函数:
1 2 3 4 5 6 TEST (SplitChineseRmStopWordsTests, HandlesEmptyString) { SplitChinese splitter ("sources/project/stop/stop_words_zh.txt" , "conf/jieba.json" ) ; std::string input = "" ; std::string result = splitter.rmStopWords (input); EXPECT_EQ (result, "" ); }
然后在 CMakeLists.txt 文件中注册:
1 2 3 4 5 6 7 # 为 SplitChineseTest 创建测试add_executable (SplitChineseTest SplitChineseTest.cpp)target_link_libraries (SplitChineseTest PRIVATE gtest_main gtest pthread) # 注册测试include (GoogleTest)gtest_discover_tests (SplitChineseTest)
如果测试的这个函数依赖相关的头文件和源文件一定要包含进来。
因此,一般我的文件结构是这样的:
1 2 3 4 5 6 7 8 test/ ├── CMakeLists.txt ① ├── SplitChineseTest │ ├── CMakeLists.txt ② │ └── SplitChineseTest.cpp └── SplitEnglishTest ├── CMakeLists.txt ③ └── SplitEnglishTest.cpp
如果我不需要再对某个 cpp 文件进行测试,只需要在 ① 中把包含的 ② 或 ③
取消掉即可。
1 2 add_subdirectory (SplitEnglishTest) // 依旧进行测试
关键注意事项:
enable_testing()
是必须的 ,否则无法注册和运行测试。
如果项目有多个子目录,每个目录的 CMakeLists.txt
文件要正确传递依赖关系。
推荐使用
gtest_discover_tests()
,它会自动发现并注册所有测试用例。
这样,就可以在 CMake 项目中方便地集成和运行 GTest 了。
遇到的问题
我们会测试类的成员函数,因此要把本该私有的成员函数单独放一起,测试的时候就临时设置为
public,如果还需要用到某个成员变量,也是要这么做。
尽管 Gtest
有设置友元等方法,但是这污染了我们的代码,个人并不喜欢这样,如果你乐意也可以尝试,毕竟重构代码的时候随时可以测试。下面演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <gtest/gtest.h> class MyClass {private : int secret_value = 42 ; int secret_function () const { return secret_value * 2 ; }public : int public_function () const { return secret_function () + 1 ; } FRIEND_TEST (MyClassTest, AccessPrivateMembers); };
下面是我们测试的程序中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 #include <gtest/gtest.h> #include "MyClass.h" TEST (MyClassTest, AccessPrivateMembers) { MyClass my_class; EXPECT_EQ (my_class.secret_value, 42 ); EXPECT_EQ (my_class.secret_function (), 84 ); }
语法
基本概念:Test Suite 和 Test
Case
Test Suite
1 2 3 4 TEST (TestSuiteName, TestCaseName) { EXPECT_EQ (func (0 ), 0 ); }
一个文件里只能有一个
TestSuiteName,建议命名为这个文件测试的类名。
TestCaseName
是测试用例的名称。命名要有意义,能够从名称上判断行为,如果实在不可以再进行简单的注释。
TestSuiteName 和 TestCaseName
组合要唯一 。
GTest
生成的类名是带下划线的,所以上面这些名字里不建议有下划线 。
1 2 3 4 5 6 7 8 9 10 11 12 13 TEST (SplitChinese, HandlesEmptyString) { SplitChinese splitter ("sources/project/stop/stop_words_zh.txt" , "conf/jieba.json" ) ; std::string input = "" ; std::string result = splitter.rmStopWords (input); EXPECT_EQ (result, "" ); }TEST (SplitChinese, HandlesStringWithNoStopWords) { SplitChinese splitter ("sources/project/stop/stop_words_zh.txt" , "conf/jieba.json" ) ; std::string input = "这是一个测试" ; std::string result = splitter.rmStopWords (input); EXPECT_EQ (result, "这是 一个 测试" ); }
从 HandlesEmptyString
名称就看出是测试空字符,HandlesStringWithNoStopWords
是测试没有停用词的情况。
其他规则同样符合前面所讲。
Test Case
我们会有这样一种场景,就是后面程序要用的变量是一致的
,为了避免重复写初始化变量的代码,可以用这种方式进行简化,简单来说就是利用作用域。
1 2 3 4 5 6 7 8 9 10 11 12 13 TEST (Foo, bar) { Context ctx; { params.enable_refresh = true ; ASSERT_EQ (ctx->is_enable_fresh (), true ); } { 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 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_F,避免写重复的代码
前面讲的 TEST 是基本的测试宏,代表一个最小测试单元。在执行 TEST
宏时,GTest 会为每个 TEST
定义一个独立的实例,使其互相隔离,避免对同一个变量进行修改或共享等可能带来的副作用。
下面我们介绍
TEST_F,可以在多个测试用例之间共享数据结构或方法 。对于同一个
Test Suite 的所有 Test Cases,会创建一个 TestFixture 对象,其
SetUp 函数会在每个 Test Case 执行之前被调用,而
TearDown 函数则会在每个 Test Case 执行之后被调用。
将共享的变量作为成员变量,可以在 Test Case
中直接访问;变量初始化、回收逻辑放到 SetUp()、TearDown()。
提供公共方法,可以在 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 24 25 class FooTest : public ::testing::Test {protected : void SetUp () override { ctx = RequestContext ("123" ); } void TearDown () override {} Ad new_ad () { return Ad (ctx); } RequestContext ctx; };TEST_F (FooTest, enable_foo) { ctx->params.enable_foo = true ; auto item = new_ad (); ... }TEST_F (FooTest, OnTestProgramStart) { }
断言:EXPECT 与 ASSERT 宏
如果某个判断不通过时,会影响后续步骤,要使用
ASSERT。常见的是空指针,或者数组访问越界。
其他情况,可以使用 EXPECT,尽可能多测试几个用例。
EXPECT 断言失败会继续往下执行,但是 ASSERT 断言失败会终止。
很多方式可见:http://google.github.io/googletest/reference/assertions.html
下面介绍常用的 EXPECT 断言,ASSERT 只需要进行替换即可:
基本断言
EXPECT_TRUE(condition)
// 条件为 true
时断言成功
EXPECT_FALSE(condition)
// 条件为 false
时断言成功
比较断言
EXPECT_EQ(val1, val2)
// 检查两值是否相等
EXPECT_NE(val1, val2)
// 检查两值是否不相等
EXPECT_LT(val1, val2)
// 检查
val1 < val2
EXPECT_LE(val1, val2)
// 检查
val1 <= val2
EXPECT_GT(val1, val2)
// 检查
val1 > val2
EXPECT_GE(val1, val2)
// 检查
val1 >= val2
字符串断言
EXPECT_STREQ(str1, str2)
// 检查 C 字符串是否相等
EXPECT_STRNE(str1, str2)
// 检查 C
字符串是否不相等
EXPECT_STRCASEEQ(str1, str2)
// 检查 C
字符串(忽略大小写)是否相等
EXPECT_STRCASENE(str1, str2)
// 检查 C
字符串(忽略大小写)是否不相等
浮点数断言
EXPECT_FLOAT_EQ(val1, val2)
// 检查两个
float
是否近似相等
EXPECT_DOUBLE_EQ(val1, val2)
// 检查两个
double
是否近似相等
EXPECT_NEAR(val1, val2, abs_error)
//
检查两个浮点数是否在误差范围内
异常断言
EXPECT_THROW(statement, type)
//
检查代码是否抛出了指定类型的异常
EXPECT_ANY_THROW(statement)
//
检查代码是否抛出了任意异常
EXPECT_NO_THROW(statement)
//
检查代码是否未抛出任何异常
补充说明
自定义消息
所有断言支持自定义输出消息,形式为:
1 EXPECT_EQ (x, y) << "x 和 y 不相等,x=" << x << ", y=" << y;
复合断言
将多个断言组合在一个测试用例中。例如:
1 2 3 4 5 TEST (SampleTest, CombinedAssertions) { ASSERT_TRUE (5 > 3 ); EXPECT_EQ ("hello" , std::string ("hello" )); ASSERT_NEAR (0.1 , 0.1001 , 0.001 ); }
技巧
编译参数
访问私有变量:-fno-access-control
,放在单测的 optimize
参数里。
修改 const 字段:使用 const_cast<Type&>
修改常量类型。
优化级别改为 O0
:单测覆盖率报告更准。
运行单侧
运行特定单测
1 --gtest_filter=[TestSuiteName.TestName]
示例:
1 2 3 4 5 6 7 8 9 10 11 ./test --gtest_filter=MyTestSuite.MyTest ./test --gtest_filter=MyTestSuite.* ./test --gtest_filter=TestSuite1. Test1:TestSuite2. Test2 ./test --gtest_filter=-TestSuite1. Test1
重复运行单测多次
有些单元测试涉及到多线程,可能会偶发性的不通过。
1 --gtest_repeat=[repeat_count]
运行测试 10 次:
1 2 3 ./test --gtest_repeat=10
1 2 3 ./test --gtest_repeat=10 --gtest_break_on_failure
1 2 3 ./test --gtest_repeat=-1
临时禁用某个单测
可以使用DISABLED_
前缀来跳过某项测试:
1 2 3 4 5 TEST_F (DISABLED_BarTest, DoesXyz) { ... } TEST_F (BarTest, DISABLED_DoesXyz) { ... }
单测书写规范
目录结构、文件与命名规范
单测的目录结构,要和源码的目录结构一致
单测文件的路径名,等价于源码的文件名加上 _test
后缀。
目的在于:让写单测的人能很快定位是否已经有这个文件或这个类的单测,让新增代码更聚合,避免写重复单测。
1 2 3 4 5 6 7 8 9 10 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 TEST (RequestContextTest, foo) {...}
TestCase
建议命名为被测试的函数名,不要随意起名,也不需要增加不必要的前缀:
1 2 3 4 TEST (RequestContextTest, init_uav_to_group_bid) { ASSERT_EQ (ctx->init_uav_to_group_bid (), 1 ); }
GTest 生成的类名是带下划线的,所以上面这些名字建议用驼峰形式。
写优雅的、可理解的、易于维护的单测:代码风格与注释
不要用
std::cout 输出变量值,改为用 ASSERT
/ EXPECT
检查
能用 EXPECT 就不要写 std::cout:
如果 cout 的日志是确定性的,那么应该写成断言。
如果是 debug 用的,那么在写完单测后应该删除。
如果期望单测失败时打印,那么应该放在
EXPECT_CALL()... << ...
后面,而不是直接输出。
除此之外,这些日志没有任何意义 ,只会刷屏,没有保留的必要。
1 2 EXPECT_EQ (rsp.size (), 1 ); EXPECT_EQ (rsp.size (), 1 ) << rsp.ads.debug_string () << std::endl;
不要直接写数值,要写清楚这个数字是怎么算的
直接写一个数字
2965
,其他人并不知道这个数字是怎么算出来的,后续有问题也不好排查。
写出这个数字的计算过程,映射到代码分支上 ,其他人好看懂。这也是白盒化单测的表现之一。
1 2 3 4 5 6 7 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 );
为单测补充详细的注释
单测写出来必须的白盒的、可理解的、可维护的。如果不补充注释,其他人根本看不懂这些单测在测试什么逻辑,也无法确保其有效,后续修单测也很痛苦。
为单测补充注释时,重点要说明「这些赋值对应了哪个分支条件」,目标是让其他人扫一眼源码就能知道这些单测在测试哪些逻辑。
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 );
写稳定的单测
单测里禁止访问外部服务 ,最好是整个单测能够断网。