现在我们关注CMake项目中的测试部分,具体包括GoogleTest和CTest的使用。

概述

Google Test(简称为 gtest)是 Google 开发的一个开源的 C++ 测试框架,用于编写和运行单元测试、集成测试和功能测试。主要特点包括:

  • 支持各种平台和编译器,包括 Linux、Windows 和 macOS,并且与主流的 C++ 编译器兼容。
  • 提供了丰富的断言宏,如 EXPECT_EQASSERT_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
2
3
4
5
6
7
8
|-bin/
|-include/
|-gtest/
|-lib/
|-src/
|-demo.h
|-demo.cppp
|-demo_test.cpp

将前面的gtest头文件目录放在include/,将libgtest和libgtest_main这两个静态库放在lib/。

参考官方提供的样例,这里使用三个源文件:

  • demo.cpp:实现了几个函数功能
  • demo.h:配套的头文件
  • demo_test.cpp:针对demo的功能进行测试

几个源文件的内容依次为

demo.h
1
2
int Factorial(int n);
bool IsPrime(int n);
demo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "demo.h"

int Factorial(int n) {
int result = 1;
for (int i = 1; i <= n; i++) { result *= i; }

return result;
}

bool IsPrime(int n) {
if (n <= 1) return false;
if (n % 2 == 0) return n == 2;

for (int i = 3;; i += 2) {
if (i > n / i) break;

if (n % i == 0) return false;
}

return true;
}
demo_test.cpp
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
31
32
33
34
35
36
37
38
39
40
41
42
#include "demo.h"
#include "gtest/gtest.h"

#include <climits>

namespace {

TEST(FactorialTest, Negative) {
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
}

TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); }

TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}

TEST(IsPrimeTest, Negative) {
EXPECT_FALSE(IsPrime(-1));
EXPECT_FALSE(IsPrime(-2));
EXPECT_FALSE(IsPrime(INT_MIN));
}

TEST(IsPrimeTest, Trivial) {
EXPECT_FALSE(IsPrime(0));
EXPECT_FALSE(IsPrime(1));
EXPECT_TRUE(IsPrime(2));
EXPECT_TRUE(IsPrime(3));
}

TEST(IsPrimeTest, Positive) {
EXPECT_FALSE(IsPrime(4));
EXPECT_TRUE(IsPrime(5));
EXPECT_FALSE(IsPrime(6));
EXPECT_TRUE(IsPrime(23));
}
} // namespace

在项目根目录下使用如下命令编译(确保编译器和编译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
24
Running 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
2
3
4
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

解释一下:这里的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
2
3
4
5
6
7
8
9
10
11
if(<condition>) {
SUCCEED();
} else {
FAIL();
}

if(<condition>) {
SUCCEED();
} else {
ADD_FAILURE();
}

在效果上分别等同于ASSERT_TRUE(<condition>)EXPECT_TRUE(<condition>)

断言宏的使用例如

1
2
3
4
5
EXPECT_EQ(1, Factorial(-5));

EXPECT_EQ(1, Factorial(-1));

EXPECT_GT(Factorial(-10), 0);

断言宏在失败会显示如下信息:

  • 失败的EXPECT_*/ASSERT_*语句位置;
  • 参数的期待值和具体值

例如

1
2
3
4
src/demo_test.cpp:38: Failure
Value of: IsPrime(10)
Actual: false
Expected: true

有时我们还需要在某个断言失败时补充一些错误说明信息,可以直接使用<<输出说明信息,例如

1
2
3
4
5
ASSERT_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
2
3
4
5
TEST(FactorialTest, Negative) {
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
}

这里 Negative 是测试单元的名称,而 FactorialTest 是测试单元所属的分组,完整名称为 FactorialTest.Negative 。

TEST()宏里面除了断言宏,还可以加入其他任意的合法执行语句,例如

1
2
3
4
5
6
7
8
9
10
TEST(testcase, test_expect)
{
std::cout << "add function start" << std::endl;
EXPECT_EQ(add(1,2), 2);
std::cout << "add function end" << std::endl;

std::cout << "sub function start" << std::endl;
EXPECT_EQ(sub(1,2), -1);
std::cout << "sub function end" << std::endl;
}

如果某个测试单元目前存在某些问题,可以在名称中加上DISABLED_前缀,表示测试单元被禁用,默认情况下不会执行被禁用的测试单元。(但是被禁用的测试单元在加上特定命令行参数后仍然可以执行,见下文)

1
2
3
4
5
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
12
class 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <gtest/gtest.h>

#include "something_test.h"

TEST(group1,unit1){
// ...
}

TEST(group1,unit2){
// ...
}

TEST(group2,unit1){
// ...
}

TEST(group2,unit2){
// ...
}

由于 GoogleTest 本身是基于 C++ 的,在测试 C 实现的代码时还要使用 extern "C",例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <gtest/gtest.h>
extern "C" {
#include "something_test.h"
}

TEST(group1,unit1){
// ...
}

TEST(group1,unit2){
// ...
}

TEST(group2,unit1){
// ...
}

TEST(group2,unit2){
// ...
}

这里的测试文件不含有main函数,因此我们可以合并使用多个测试文件,并链接gtest_main来生成测试程序。

gtest命令行选项

下面是gtest生成的测试程序所支持的命令行参数:

  1. --help:显示帮助信息,列出可用的命令行选项。
  2. --gtest_filter=TEST_PATTERN:指定要运行的测试单元或测试单元的匹配模式,只有匹配的测试单元才会被运行。
  3. --gtest_repeat=COUNT:指定要重复运行的测试次数。
  4. --gtest_shuffle:在运行测试之前随机排序测试单元。这有助于检测测试单元之间的依赖性。
  5. --gtest_break_on_failure:如果有测试单元失败,则停止测试执行。
  6. --gtest_color=yes|no|auto:启用或禁用彩色输出。auto 选项将尝试自动检测是否支持彩色终端。
  7. --gtest_output=xml[:DIRECTORY_PATH/]TEST_PREFIX.xml:指定将测试结果输出为 XML 格式,并可选地指定输出目录和文件前缀。
  8. --gtest_also_run_disabled_tests:运行被禁用的测试单元。

其中的匹配规则例如:--gtest_filter=FooTest.*-FooTest.Bar,表明运行所有分组为FooTest的测试单元,但是FooTest.Bar除外。

CTest

简单示例

首先,我们生成一个简单的CMake项目, 项目架构如下

1
2
3
4
5
6
7
8
|-bin/
|-src/
|-demo/
|-demo.h
|-demo.cpp
|-test/
|-main_in.cpp
|-main_self.cpp

包括一个库demo,提供函数bool IsPrime(int n);来判断输入的数据是否是素数。 以及两个可执行文件main_inmain_self,它们的作用是判断一个或一组输入的整数是不是素数:

  • 如果输入非法,即输入不全是非负整数,则返回-1
  • 如果输入合法:
    • 输入数据全是素数,返回0
    • 输入数据不全是素数,返回合数的个数,即非零值

其中main_in需要从命令行参数读取数据,而main_self在main函数中直接提供数据。

源文件依次为:

demo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "demo.h"

bool IsPrime(int n) {
if (n <= 1) return false;
if (n % 2 == 0) return n == 2;

for (int i = 3;; i += 2) {
if (i > n / i) break;

if (n % i == 0) return false;
}

return true;
}
main_self.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cstdlib>
#include <vector>

#include "demo.h"

int main(int argc, char *argv[]) {
std::vector<int> numbers{2, 3, 5, 7};

int result = 0;
for (auto &num : numbers) {
if (!IsPrime(num)) { result++; }
}

return result;
}
main_in.cpp
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
#include <cstdlib>
#include <iostream>
#include <vector>

#include "demo.h"

int main(int argc, char *argv[]) {
if (argc == 1) {
std::cerr << "Usage: " << argv[0] << " num1 [num2 ...]\n";
return -1;
}

std::vector<int> numbers;

for (int i = 1; i < argc; ++i) {
int num = std::atoi(argv[i]);
if (num <= 0) {
std::cerr << "Invalid input: " << num << " is negative.\n";
return -1;
}
numbers.push_back(num);
}

int result = 0;
for (auto &num : numbers) {
if (!IsPrime(num)) { result++; }
}

return result;
}

在项目根目录的CMakeLists.txt使用如下片段,只有在开启测试选项BUILD_TESTING时(默认开启)才会进入测试目录,编译测试目标并支持测试

1
2
3
4
5
6
7
add_subdirectory(src/demo)

include(CTest)

if(BUILD_TESTING)
add_subdirectory(src/test)
endif()

在test目录下的CMakeLists.txt为

1
2
3
4
5
6
7
8
9
10
add_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
2
cd ./build/
ctest

注意,如果编译时采用multi-configuration generator例如Visual Studio,直接执行ctest可能会找不到测试,报错信息如下

1
Test not available without configuration.  (Missing "-C <config>"?)

此时需要加上构建类型才能正常执行

1
2
cd ./build/
ctest -C Release

测试输出信息如下,一共4个测试,通过了3个,失败了1个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Test project ~/demo/build
Start 1: test1
1/4 Test #1: test1 ............................ Passed 0.77 sec
Start 2: test2
2/4 Test #2: test2 ............................***Failed 0.01 sec
Start 3: test3
3/4 Test #3: test3 ............................ Passed 0.01 sec
Start 4: test4
4/4 Test #4: test4 ............................ Passed 0.13 sec

75% tests passed, 1 tests failed out of 4

Total Test time (real) = 0.92 sec

The following tests FAILED:
2 - test2 (Failed)
Errors while running CTest
Output from these tests are in: ~/demo/build/Testing/Temporary/LastTest.log
Use "--rerun-failed --output-on-failure" to re-run the failed cases verbosely.

支持测试

使用CMake进行测试,首先必须在根目录下的CMakeLists.txt中开启测试选项,使用如下两种方式均可

1
2
3
4
enable_testing()

# or
include(CTest)

如果导入CTest模块则不需要前面的语句,因为后者在内部包括了前面的语句,并且支持更复杂的功能。

在哪一级CMakeLists.txt开启测试,就会在build中的对应位置生成CTestTestfile.cmake。 ctest命令需要找到这个文件来执行测试,建议在根目录下开启测试,那么就可以在build/目录下执行ctest命令。 如果在test/目录下开启测试,则必须进入到build/test/才可以执行ctest命令,或者给ctest指定工作目录参数--test-dir <dir>,默认值就是当前目录。

CTest模块会自动创建一个名为BUILD_TESTING的选项(默认值为ON,默认开启测试),可以使用下面的代码片段来包含测试部分的配置

1
2
3
4
5
6
include(CTest)

if(BUILD_TESTING)
# ... CMake code to create tests ...
add_subdirectory(tests)
endif()

如果考虑到当前CMake项目可能被作为子项目使用,但是作为子项目时并不需要开启测试,可以加上额外的判断,例如

1
2
3
4
5
6
include(CTest)

if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
# ... CMake code to create tests ...
add_subdirectory(tests)
endif()

添加测试

在CMakeLists.txt中主要使用add_test()命令添加测试,命令原型如下

1
2
3
4
add_test(NAME <name> COMMAND <command> [<arg>...]
[CONFIGURATIONS <config>...]
[WORKING_DIRECTORY <dir>]
[COMMAND_EXPAND_LISTS])

这个命令在各级的CMakeLists.txt中均可使用。使用例如

1
2
3
add_executable(TestInstantiator TestInstantiator.cxx)
target_link_libraries(TestInstantiator vtkCommon)
add_test(NAME TestInstantiator COMMAND TestInstantiator)

默认情况下,add_test()添加的测试命令在同时满足如下条件时视为通过:

  • 命令使用的可执行程序被正确找到
  • 测试中没有发生抛出异常,意外终止等
  • 返回值为0

当然测试需求远不止于此,我们可以使用set_property()命令修改相应的要求

1
2
set_property(TEST test_name
PROPERTY prop1 value1 value2 ...)

包括:添加环境变量,测试结果翻转(即断言当前测试将要失败),添加标签,以及使用正则匹配来重新设置当前测试的成功和失败要求。(两者同时设置时失败匹配规则优先)

1
2
3
4
5
6
7
8
9
add_test(NAME outputTest COMMAND outputTest)

set(passRegex "^Test passed" "^All ok")
set(failRegex "Error" "Fail")

set_property(TEST outputTest
PROPERTY PASS_REGULAR_EXPRESSION "${passRegex}")
set_property(TEST outputTest
PROPERTY FAIL_REGULAR_EXPRESSION "${failRegex}")

我们甚至还可以将构建并编译一个CMake项目也作为测试的一部分。(CMake就是这样进行自我测试的)

例如当前主项目名为 MainProject,并且附加一个 examples/simple 项目需要在测试时构建,可以如下实现:

1
2
3
4
5
6
7
8
9
10
add_test(
NAME
ExampleCMakeBuild
COMMAND
"${CMAKE_CTEST_COMMAND}"
--build-and-test "${My_SOURCE_DIR}/examples/simple"
"${CMAKE_CURRENT_BINARY_DIR}/simple"
--build-generator "${CMAKE_GENERATOR}"
--test-command "${CMAKE_CTEST_COMMAND}"
)

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
6
FetchContent_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
7
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()

GoogleTest导入配置模板

get_gtest.cmake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
include(CTest)

# fetch from URL
include(FetchContent)
FetchContent_Declare(
googletest # googletest-1.14.0
URL https://github.com/google/googletest/archive/f8d7d77c06936315286eb55f8de22cd23c188571.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) # Disable GMock
FetchContent_MakeAvailable(googletest)

# or locally
# add_subdirectory(googletest-1.14.0)

综合示例与使用

创建如下的项目架构

1
2
3
4
5
6
|-src/
|-demo/
|-demo.h
|-demo.cppp
|-test/
|-demo_test.cpp

源文件与前面单独使用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
29
add_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
3
add_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
2
include(GoogleTest)
gtest_discover_tests(demo_test)

在CMakeLists.txt中的内容可以改为

1
2
3
4
add_executable(demo_test demo_test.cpp)
target_link_libraries(demo_test PRIVATE demo gtest_main)
include(GoogleTest)
gtest_discover_tests(demo_test)

CMake在早期还提供了类似的gtest_add_tests()命令,但是现在不建议使用,gtest_discover_tests()是更好的选择。

使用gtest_discover_tests()时,VSCode的CMakeTools插件有关于ctest测试名称的BUG,必须手动执行CMake: Refresh Tests刷新一下。