GoogleTest + CTest 配置与使用
现在我们关注CMake项目中的测试部分,具体包括GoogleTest和CTest的使用。
概述
Google Test(简称为 gtest)是 Google 开发的一个开源的 C++ 测试框架,用于编写和运行单元测试、集成测试和功能测试。主要特点包括:
- 支持各种平台和编译器,包括 Linux、Windows 和 macOS,并且与主流的 C++ 编译器兼容。
- 提供了丰富的断言宏,如
EXPECT_EQ
、ASSERT_TRUE
等,用于验证代码行为是否符合预期。 - 支持参数化测试,允许以不同的参数运行同一个测试用例。
- 可以生成详细的测试报告,包括测试通过的数量、失败的数量、失败的原因等信息。
- 可以扩展测试框架,编写自定义的测试扩展和断言宏。
CTest 是 CMake 附带的一个测试工具,用于管理和执行项目中的测试。它是一个命令行工具,可以通过简单的命令来执行测试,并生成测试报告。主要特点包括:
- 可以在构建系统中自动发现项目中的测试,并执行它们。
- 支持各种测试框架,包括 Google Test、Catch、Boost.Test 等。
- 可以通过简单的命令行选项来控制测试的执行,如选择特定的测试配置、指定要运行的测试等。
- 可以生成各种格式的测试报告,包括文本格式和 XML 格式。
- 可以方便地与 CI/CD 工具集成,自动执行测试并生成测试报告。
这两个工具在不同层次上针对 C++ 的项目测试提供了强大的功能和灵活性,通常组合使用。
GoogleTest
编译安装
直接从GoogleTest的Github仓库下载源码,这并不是一个纯头文件的库,因此我们需要先编译这个项目,编译成功后,需要保留和使用的是如下内容:
- googletest/include/test:头文件目录,里面包括gtest.h等头文件,在测试文件中需要使用
- libgtest和libgtest_main:这两个静态库在编译后存放在build/lib中
手动安装:将头文件和库文件复制到合适的地方即可使用。
简单示例
创建如下的项目架构
1 | |-bin/ |
将前面的gtest头文件目录放在include/,将libgtest和libgtest_main这两个静态库放在lib/。
参考官方提供的样例,这里使用三个源文件:
- demo.cpp:实现了几个函数功能
- demo.h:配套的头文件
- demo_test.cpp:针对demo的功能进行测试
几个源文件的内容依次为
1 | int Factorial(int n); |
1 |
|
1 |
|
在项目根目录下使用如下命令编译(确保编译器和编译gtest时使用的一致)
1
clang++ src/*.cpp -Iinclude -Llib -lgtest -lgtest_main -o demo_test
注意到这里并没有给出main函数,并且也没有将测试样例在main函数中注册并调用,这是gtest自动完成的:
- main函数在libgtest_main中提供
- 测试样例的注册和调用是自动完成的
直接运行demo_test可以得到如下形式的结果 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24Running main() from ~/googletest-1.14.0/googletest/src/gtest_main.cc
[==========] Running 6 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from FactorialTest
[ RUN ] FactorialTest.Negative
[ OK ] FactorialTest.Negative (0 ms)
[ RUN ] FactorialTest.Zero
[ OK ] FactorialTest.Zero (0 ms)
[ RUN ] FactorialTest.Positive
[ OK ] FactorialTest.Positive (0 ms)
[----------] 3 tests from FactorialTest (1 ms total)
[----------] 3 tests from IsPrimeTest
[ RUN ] IsPrimeTest.Negative
[ OK ] IsPrimeTest.Negative (0 ms)
[ RUN ] IsPrimeTest.Trivial
[ OK ] IsPrimeTest.Trivial (0 ms)
[ RUN ] IsPrimeTest.Positive
[ OK ] IsPrimeTest.Positive (0 ms)
[----------] 3 tests from IsPrimeTest (1 ms total)
[----------] Global test environment tear-down
[==========] 6 tests from 2 test suites ran. (5 ms total)
[ PASSED ] 6 tests.
这表明所有的测试单元都顺利通过测试。
在使用时我们主要通过链接libgtest_main来添加main函数,其实也可以手动添加下面的代码(此时不再需要链接libgtest_main,还要注意避免main函数重定义)
1 | int main(int argc, char **argv) { |
解释一下:这里的main函数第一行是接收并解析命令行参数,调用RUN_ALL_TESTS()
会执行所有测试并输出结果。
断言宏
gtest的主要功能都通过宏来提供,包括如下几类最基础的断言宏:
ASSERT_*
:当断言失败时,产生致命错误、并终止当前的测试单元。EXPECT_*
:当断言失败时,产生非致命错误,不会终止当前的测试单元,会继续执行。
通常都会用EXPECT_*
,因为能在一个测试单元中暴露出更多的失败情况。
因为ASSERT_*
会在失败时终止当前的测试单元,会跳过后面进行清理工作的代码,这可能会产生内存泄露。
以EXPECT_*
为例,包括如下的常用宏:
EXPECT_TRUE(<condition>)
:断言为真EXPECT_FALSE(<condition>)
:断言为假EXPECT_EQ(a,b)
:断言a==b
EXPECT_NE(a,b)
:断言a!=b
EXPECT_GT(a,b)
:断言a>b
EXPECT_GE(a,b)
:断言a>=b
EXPECT_LT(a,b)
:断言a<b
EXPECT_LE(a,b)
:断言a<=b
除此之外,还有针对浮点数的比较
EXPECT_NEAR(a, b, ep)
:断言abs(a-b)<=ep
以及针对char *
字符串的比较
EXPECT_STREQ(str1,str2)
:断言两个字符串一样EXPECT_STRCASEEQ(str1,str2)
:忽略大小写,断言两个字符串一样EXPECT_STRNE(str1,str2)
:断言两个字符串不一样EXPECT_STRCASENE(str1,str2)
:忽略大小写,断言两个字符串不一样
注意:
- 一个NULL指针和一个空字符串
""
在字符串比较中也是不同的 - 对
std::string
的比较直接使用EXPECT_EQ
等即可,但是将其用于char *
则是在比较指针,是错误的
除了这些断言宏,我们还可以直接用宏来触发一次成功,失败或致命失败,
1 | if(<condition>) { |
在效果上分别等同于ASSERT_TRUE(<condition>)
,EXPECT_TRUE(<condition>)
。
断言宏的使用例如 1
2
3
4
5EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
断言宏在失败会显示如下信息:
- 失败的
EXPECT_*
/ASSERT_*
语句位置; - 参数的期待值和具体值
例如 1
2
3
4src/demo_test.cpp:38: Failure
Value of: IsPrime(10)
Actual: false
Expected: true
有时我们还需要在某个断言失败时补充一些错误说明信息,可以直接使用<<
输出说明信息,例如
1
2
3
4
5ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";
for (int i = 0; i < x.size(); ++i) {
EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}
测试单元
TEST 宏
我们可以用上面的断言宏和其他执行语句来组成一个测试单元,需要使用TEST()
宏来创建测试单元
- 第一个参数是测试单元所属分组(Test Suite)的名称
- 第二个参数是测试单元(Test)的名称
要求:两个名称都必须是合法的 C++ 标识符,并且它们不应包含任何下划线,属于不同分组的测试单元可以具有相同的名称。
例如
1 | TEST(FactorialTest, Negative) { |
这里 Negative 是测试单元的名称,而 FactorialTest 是测试单元所属的分组,完整名称为 FactorialTest.Negative 。
在TEST()
宏里面除了断言宏,还可以加入其他任意的合法执行语句,例如
1 | TEST(testcase, test_expect) |
如果某个测试单元目前存在某些问题,可以在名称中加上DISABLED_
前缀,表示测试单元被禁用,默认情况下不会执行被禁用的测试单元。(但是被禁用的测试单元在加上特定命令行参数后仍然可以执行,见下文)
1 | TEST(MyTestSuite, DISABLED_MyDisabledTest) { |
TEST_F 宏
在很多测试单元中我们可能需要执行公共的初始化和清理代码,可以使用TEST_F()
宏(Test
Fixture)来实现。
用法如下:
- 定义一个测试夹具类(Test Fixture Class),要求继承自
::testing::Test
,并且使用protectd
选项 - 在测试夹具类中包含初始化方法
SetUp()
和清理方法TearDown()
- 在
TEST_F()
宏的两个参数,第一个参数必须是测试夹具类的名称(作为分组名),第二个参数是测试单元名称
对于TEST_F()
测试单元,googletest将在运行时依次执行
- 创建一个新的测试夹具(Test Fixture)对象
- 调用
SetUp()
进行初始化 - 运行该
TEST_F()
的内容 - 调用
TearDown()
进行清理 - 删除该测试夹具(Test Fixture)对象
例如 1
2
3
4
5
6
7
8
9
10
11
12class FactorialTest_Fixture : public ::testing::Test {
protected:
void SetUp() override { std::cout << "setup\n"; }
void TearDown() override { std::cout << "teardown\n"; }
};
TEST_F(FactorialTest_Fixture, Negative) {
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
}
测试文件
一个测试文件中可以含有多个测试单元,例如
1 |
|
由于 GoogleTest 本身是基于 C++ 的,在测试 C 实现的代码时还要使用
extern "C"
,例如
1 |
|
这里的测试文件不含有main函数,因此我们可以合并使用多个测试文件,并链接gtest_main来生成测试程序。
gtest命令行选项
下面是gtest生成的测试程序所支持的命令行参数:
--help
:显示帮助信息,列出可用的命令行选项。--gtest_filter=TEST_PATTERN
:指定要运行的测试单元或测试单元的匹配模式,只有匹配的测试单元才会被运行。--gtest_repeat=COUNT
:指定要重复运行的测试次数。--gtest_shuffle
:在运行测试之前随机排序测试单元。这有助于检测测试单元之间的依赖性。--gtest_break_on_failure
:如果有测试单元失败,则停止测试执行。--gtest_color=yes|no|auto
:启用或禁用彩色输出。auto 选项将尝试自动检测是否支持彩色终端。--gtest_output=xml[:DIRECTORY_PATH/]TEST_PREFIX.xml
:指定将测试结果输出为 XML 格式,并可选地指定输出目录和文件前缀。--gtest_also_run_disabled_tests
:运行被禁用的测试单元。
其中的匹配规则例如:--gtest_filter=FooTest.*-FooTest.Bar
,表明运行所有分组为FooTest的测试单元,但是FooTest.Bar除外。
CTest
简单示例
首先,我们生成一个简单的CMake项目, 项目架构如下
1 | |-bin/ |
包括一个库demo
,提供函数bool IsPrime(int n);
来判断输入的数据是否是素数。
以及两个可执行文件main_in
和main_self
,它们的作用是判断一个或一组输入的整数是不是素数:
- 如果输入非法,即输入不全是非负整数,则返回
-1
- 如果输入合法:
- 输入数据全是素数,返回
0
- 输入数据不全是素数,返回合数的个数,即非零值
- 输入数据全是素数,返回
其中main_in
需要从命令行参数读取数据,而main_self
在main函数中直接提供数据。
源文件依次为:
1 |
|
1 |
|
1 |
|
在项目根目录的CMakeLists.txt使用如下片段,只有在开启测试选项BUILD_TESTING
时(默认开启)才会进入测试目录,编译测试目标并支持测试
1 | add_subdirectory(src/demo) |
在test目录下的CMakeLists.txt为 1
2
3
4
5
6
7
8
9
10add_executable(main_in main_in.cpp)
target_link_libraries(main_in PRIVATE demo)
add_executable(main_self main_self.cpp)
target_link_libraries(main_self PRIVATE demo)
add_test(NAME test1 COMMAND main_in 2)
add_test(NAME test2 COMMAND main_in -2)
add_test(NAME test3 COMMAND main_in 2 5)
add_test(NAME test4 COMMAND main_self)
其中add_test
命令添加了四个测试,分别指定了测试名称和测试要执行的命令,命令包括执行的target名称(会自动替换为完整的可执行文件名)和命令行参数。
在编译完成后,我们需要进入构建目录 ./build
来执行测试
1 | cd ./build/ |
注意,如果编译时采用multi-configuration generator例如Visual
Studio,直接执行ctest可能会找不到测试,报错信息如下 1
Test not available without configuration. (Missing "-C <config>"?)
此时需要加上构建类型才能正常执行 1
2cd ./build/
ctest -C Release
测试输出信息如下,一共4个测试,通过了3个,失败了1个。
1 | Test project ~/demo/build |
支持测试
使用CMake进行测试,首先必须在根目录下的CMakeLists.txt中开启测试选项,使用如下两种方式均可
1 | enable_testing() |
如果导入CTest模块则不需要前面的语句,因为后者在内部包括了前面的语句,并且支持更复杂的功能。
在哪一级CMakeLists.txt开启测试,就会在build中的对应位置生成
CTestTestfile.cmake
。 ctest命令需要找到这个文件来执行测试,建议在根目录下开启测试,那么就可以在build/
目录下执行ctest命令。 如果在test/
目录下开启测试,则必须进入到build/test/
才可以执行ctest命令,或者给ctest指定工作目录参数--test-dir <dir>
,默认值就是当前目录。
CTest模块会自动创建一个名为BUILD_TESTING
的选项(默认值为ON
,默认开启测试),可以使用下面的代码片段来包含测试部分的配置
1 | include(CTest) |
如果考虑到当前CMake项目可能被作为子项目使用,但是作为子项目时并不需要开启测试,可以加上额外的判断,例如
1 | include(CTest) |
添加测试
在CMakeLists.txt中主要使用add_test()
命令添加测试,命令原型如下
1
2
3
4add_test(NAME <name> COMMAND <command> [<arg>...]
[CONFIGURATIONS <config>...]
[WORKING_DIRECTORY <dir>]
[COMMAND_EXPAND_LISTS])
这个命令在各级的CMakeLists.txt中均可使用。使用例如 1
2
3add_executable(TestInstantiator TestInstantiator.cxx)
target_link_libraries(TestInstantiator vtkCommon)
add_test(NAME TestInstantiator COMMAND TestInstantiator)
默认情况下,add_test()
添加的测试命令在同时满足如下条件时视为通过:
- 命令使用的可执行程序被正确找到
- 测试中没有发生抛出异常,意外终止等
- 返回值为0
当然测试需求远不止于此,我们可以使用set_property()
命令修改相应的要求
1
2set_property(TEST test_name
PROPERTY prop1 value1 value2 ...)
包括:添加环境变量,测试结果翻转(即断言当前测试将要失败),添加标签,以及使用正则匹配来重新设置当前测试的成功和失败要求。(两者同时设置时失败匹配规则优先)
1 | add_test(NAME outputTest COMMAND outputTest) |
我们甚至还可以将构建并编译一个CMake项目也作为测试的一部分。(CMake就是这样进行自我测试的)
例如当前主项目名为 MainProject,并且附加一个 examples/simple 项目需要在测试时构建,可以如下实现:
1 | add_test( |
ctest命令行选项
在执行测试时,ctest命令也支持一些常见的命令行参数选项,例如:
- 基于正则匹配筛选:
-R <regex>
:运行名称匹配的测试-E <regex>
:排除名称匹配的测试-L <regex>
:运行标签匹配的测试-LE <regex>
:运行标签不匹配的测试
-C <config>
:选择要测试的配置。如果项目有多个配置(如 Debug、Release),则可以使用此选项选择要测试的特定配置。对于Visual Studio 这是必须添加的选项-V, --verbose
:启用来自测试的详细输出-N, --show-only
:显示将要运行的测试,但不会实际执行它们--test-dir <dir>
:指定ctest查找测试的目录,默认是在当前目录
CMake导入GoogleTest
在前文中,我们手动下载GoogleTest源码进行编译,并手动安装了需要的头文件和库文件, 现在考虑如何在CMake项目中添加并使用GoogleTest,有很多种方法可以做到。
基于URL添加源码
我们可以使用FetchContent
模块:基于FetchContent
模块的FetchContent_Declare
命令,它会在生成项目时,进行源码的拉取。(需要保证网络的畅通)
在项目根目录的CMakeLists.txt中添加如下片段 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# build tests if BUILD_TESTING is ON
include(CTest)
if (BUILD_TESTING)
include(FetchContent)
# gtest version release-1.11.0
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/e2239ee6043f73722e7aa812a459f54a28552929.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) # Disable GMock
FetchContent_MakeAvailable(googletest)
add_subdirectory(tests)
endif()
这个片段是从微软的proxy库抄的,但是这里1.11.0版本的googletest对cmake要求太低,cmake会报警告,下面换成1.14.0就没有警告了。
将URL中1.11.0版本的哈希值替换为最新版1.14.0的哈希值
1
2
3
4
5
6# gtest version release-1.14.0
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/f8d7d77c06936315286eb55f8de22cd23c188571.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
也可以从哈希值形式的压缩包URL换成下面这种形式来直接下载最新版本,注意默认是master因此这里要注明main
1
2
3
4
5
6FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG main
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
实践中发现,采用含哈希值的URL更容易下载到本地,直接采用不含哈希值的URL则可能受到墙的网络问题影响,更难下载到本地。
本地下载添加源码
我们同样可以手动下载googletest的完整源码,并复制到当前项目的根目录下作为子文件夹, 作为当前项目的子项目,这免去了每次从网络下载的过程。
在根目录下的CMakeLists.txt使用如下片段 1
2
3
4
5
6
7include(CTest)
if (BUILD_TESTING)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) # Disable GMock
add_subdirectory(googletest-1.14.0)
add_subdirectory(test)
endif()
GoogleTest导入配置模板
1 | include(CTest) |
综合示例与使用
创建如下的项目架构
1 | |-src/ |
源文件与前面单独使用GoogleTest时的一样,注意目前并没有在本地下载GoogleTest。
在根目录下的CMakeLists.txt,包含如下的片段 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
29add_subdirectory(src/demo)
# build tests if BUILD_TESTING is ON
include(CTest)
if (BUILD_TESTING)
include(FetchContent)
# gtest version release-1.14.0
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/f8d7d77c06936315286eb55f8de22cd23c188571.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) # Disable GMock
FetchContent_MakeAvailable(googletest)
add_subdirectory(test)
endif()
# or
# include(CTest)
# if (BUILD_TESTING)
# set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
# set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) # Disable GMock
# add_subdirectory(googletest-1.14.0)
# add_subdirectory(test)
# endif()
在test目录下的CMakeLists.txt为 1
2
3add_executable(demo_test demo_test.cpp)
target_link_libraries(demo_test PRIVATE demo gtest_main)
add_test(NAME google_test COMMAND demo_test)
这里我们组合使用了GoogleTest和CTest:
- GoogleTest是源码级的,用于编写具体的单元测试内容,并生成
demo_test
可执行程序; - CTest是命令级的,创建一个执行
demo_test
程序的测试。
注意:demo_test只链接了gtest_main,并没有链接gtest,不清楚这种做法的区别。
我们显然有两种做法来执行测试:
- 第一种方法:完全绕过CTest,直接执行单元测试的可执行文件,
./bin/demo_test
- 第二种方法:进入build目录,执行
ctest
但是在执行ctest
时,我们会发现显示只有一个测试,因为这里使用add_test()
命令把整个demo_test视作单个CTest测试,
完全忽略了gtest的完整测试信息。为了解决这个问题,CMake提供下面的gtest_discover_tests()
命令,从使用gtest的可执行文件中读取gtest的完整测试信息,并分别把每一个测试对接给ctest,这样ctest就会识别出gtest的每一个单元测试。(这就完全替代了add_test()
命令)
1 | include(GoogleTest) |
在CMakeLists.txt中的内容可以改为
1 | add_executable(demo_test demo_test.cpp) |
CMake在早期还提供了类似的
gtest_add_tests()
命令,但是现在不建议使用,gtest_discover_tests()
是更好的选择。
使用
gtest_discover_tests()
时,VSCode的CMakeTools插件有关于ctest测试名称的BUG,必须手动执行CMake: Refresh Tests
刷新一下。