CMake 入门教程之三单元测试

前言

CMake 是一个跨平台的 C/C++ 项目组织管理工具,虽然许多 IDE 都有私有的项目管理工具,但是在现在各大 IDE 基本都支持使用 CMake 管理项目,所以如果有跨平台的需求,使用 CMake 管理是最方便的。值得一提的是,CMake 支持 gtestcppunit 等单元测试框架,当然也可以使用断言自定义单元测试。

创建简单的带单元测试的项目

创建项目工程

下载代码

点击下载 完整的案例代码,项目的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
minder-test
├── CMakeLists.txt
├── include
│ └── datetime.h
├── src
│ ├── datetime.cpp
│ └── main.cpp
└── test
├── CMakeLists.txt
├── include
│ └── strUtil.h
└── src
└── main.cpp

编写项目代码

  • include/datetime.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once

#include <iostream>
#include <sstream>

using namespace std;

// 日期工具类
class DateUtil {

public:

static string formatCurrentTime();

static string formatCurrentTime(string format);

static int dayOfWeek(const string &date);

static bool isWeekendDays(const string &date);
};
  • src/datetime.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
43
44
45
46
47
48
49
50
51
#include "datetime.h"

// 格式化当前时间
// 默认格式是: 2020-06-07 23:46:53
string DateUtil::formatCurrentTime() {
time_t rawtime;
struct tm *info;
char buffer[80];

time(&rawtime);
info = localtime(&rawtime);
strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", info);
string str(buffer);
return str;
}

// 格式化当前时间
// format: 格式字符串,例如 %Y-%m-%d %H:%M:%S
string DateUtil::formatCurrentTime(string format) {
time_t rawtime;
struct tm *info;
char buffer[80];

time(&rawtime);
info = localtime(&rawtime);
strftime(buffer, 80, format.c_str(), info);
string str(buffer);
return str;
}

// 根据给定的日期,计算它是星期几
// date: 日期字符串,格式是: 2021-12-01
// 返回值:1, 2, 3, 4, 5, 6, 0, 其中 0 表示星期日
int DateUtil::dayOfWeek(const string &date) {
char c;
int y, m, d;
stringstream(date) >> y >> c >> m >> c >> d;
tm t = {0, 0, 0, d, m - 1, y - 1900};
mktime(&t);
return t.tm_wday;
}

// 根据给定的日期,判断是否为周末
// date: 日期字符串,格式是: 2021-12-01
bool DateUtil::isWeekendDays(const string &date) {
int wday = dayOfWeek(date);
if (wday == 6 || wday == 0) {
return true;
}
return false;
}
  • src/main.cpp
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include "datetime.h"

using namespace std;

int main() {
cout << DateUtil::formatCurrentTime() << endl;
cout << DateUtil::formatCurrentTime("%Y-%m-%d") << endl;
return 0;
}
  • test/include/strUtil.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once

#include <iostream>

using namespace std;

// 去除字符串两边的空格
void trim(string &str) {
if (str.empty()) {
return;
}
str.erase(0, str.find_first_not_of(" "));
str.erase(str.find_last_not_of(" ") + 1);
}
  • test/src/main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include "strUtil.h"
#include "datetime.h"

using namespace std;

int main() {
// 去除字符串两边的空格
string str = " Hello World ! ";
trim(str);
cout << str << endl;

// 根据给定的日期,计算它是星期几
cout << "wday = " << DateUtil::dayOfWeek("2022-01-11") << ", ";
cout << "isWeekendDays = " << (DateUtil::isWeekendDays("2022-01-11") ? "true" : "false") << endl;
return 0;
}

其中 test 目录可以视作为子项目,和主目录分开编译。为了模拟更真实的企业项目开发场景,这里的 test/src/main.cpp 同时引入了 datetime.hstrUtil.h 头文件。

CMake 配置文件

  • 主目录的 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
30
31
32
33
34
cmake_minimum_required(VERSION 3.15)

# 项目信息
project(minder)

# 定义C++的版本
set(CMAKE_CXX_STANDARD 11)

# 输出调试信息
set(CMAKE_CXX_FLAGS "-g")

# 开启所有警告
set(CMAKE_CXX_FLAGS "-Wall")

# 指定构建输出的目录
set(PROJECT_BINARY_DIR ${PROJECT_SOURCE_DIR}/build)

# 引入主项目的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索主项目的源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/src MAIN_SOURCES)

# 指定可执行文件的名称和主项目的所有源文件
add_executable(${PROJECT_NAME} ${MAIN_SOURCES})

# 启用项目测试
enable_testing()

# 添加子目录(测试项目)
add_subdirectory(test)

# 添加测试项目的可执行文件
add_test(minder_test ${PROJECT_SOURCE_DIR}/test/build/minder_test)

特别说明:

  • set(CMAKE_CXX_FLAGS "-xxx"):指定编译参数,细化的还有 CMAKE_CXX_FLAGS_DEBUGCMAKE_CXX_FLAGS_RELEASE
  • add_subdirectory(xxx):添加子目录(子项目),要求子目录里必须有单独的 CMakeLists.txt,该文件包含了子目录的编译配置信息
  • add_test(xxx ${PROJECT_SOURCE_DIR}/test/build/xxx):第一个参数是某个单元测试的名称,第二个参数是该单元测试的可执行文件的路径

  • test 目录的 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
cmake_minimum_required(VERSION 3.15)

# 项目信息
project(minder_test)

# 定义C++的版本
set(CMAKE_CXX_STANDARD 11)

# 搜索父目录(父项目)的头文件
include_directories(../include)

# 搜索父目录(父项目)的源文件
aux_source_directory(../src MAIN_SOURCES)

# 排除父目录(父项目)的入口源文件
list(FILTER MAIN_SOURCES EXCLUDE REGEX "main.cpp")

# 引入子项目的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索子项目里的源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/src TEST_SOURCES)

# 指定构建输出的目录
set(PROJECT_BINARY_DIR ${PROJECT_SOURCE_DIR}/build)

# 指定可执行文件的名称和单元测试的所有源文件
add_executable(${PROJECT_NAME} ${MAIN_SOURCES} ${TEST_SOURCES})

这里的 test 作为子项目,主要要生成单元测试的可执行文件。

命令行编译项目

  • 编译 test 子项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 进入子项目的目录
$ cd minder-test/test

# 创建子项目的构建目录
$ mkdir build

# 进入子项目的构建目录
$ cd build

# 构建子项目
$ cmake ..

# 编译子项目
$ make

# 运行可执行文件
$ ./minder_test
  • 编译主项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 进入主项目的目录
$ cd minder-test

# 创建主项目的构建目录
$ mkdir build

# 进入主项目的构建目录
$ cd build

# 构建主项目
$ cmake ..

# 编译主项目
$ make

# 执行项目测试
$ make test

# 运行可执行文件
$ ./minder

CMake 使用 GoogleTest 测试框架

GoogleTest 的安装

GoogleTest 编译安装

注意事项

  1. GoogleTest 最新版(1.11.0)要求使用 GCC 5.0+ 和 Clang 5.0+,若 GCC 的版本比较低,建议安装 GoogleTest 1.10.0 或者 1.8.1 版本
  2. 实测 GCC 4.8.5 可以正常使用 GoogleTest 的 1.10.0 版本,不兼容 1.11.0 版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 下载文件
$ wget https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz

# 解压文件
$ tar -xvf release-1.11.0.tar.gz

# 进入解压目录
$ cd googletest-release-1.11.0

# 创建构建目录
$ mkdir build

# 进入构建目录
$ cd build

# 生成makefile,如果需要构建得到动态链接库,则必须添加参数 "-DBUILD_SHARED_LIBS=ON",否则默认只会得到静态库(.a)
$ cmake -DBUILD_SHARED_LIBS=ON -Dgtest_build_samples=ON ..

# 编译
$ make -j4

# 安装
$ make install

值得一提的是,安装命令执行完成后,会自动将 libgmock_main.solibgmock.solibgtest_main.solibgtest.so 库文件拷贝到 /usr/local/lib64/ 目录下。GoogleTest 的头文件则会安装在 /usr/local/include/gmock/usr/local/include/gtest/ 目录。

GoogleTest 验证安装

  • 创建 C++ 源文件 test.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
#include <gtest/gtest.h>

TEST( COutputPopLimitStrategyTest, PositiveNos )
{
EXPECT_EQ(true, true);
}

int main( int argc, char *argv[] )
{
::testing::InitGoogleTest( &argc, argv );

return(RUN_ALL_TESTS() );
}
  • 使用 G++ 命令编译 C++ 源文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 编译源文件
$ g++ -std=c++11 test.cpp -lpthread /usr/local/lib64/libgtest.so -o test

# 运行可执行文件,若输出以下的日志信息,则说明GoogleTest安装成功
$ ./test

[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from COutputPopLimitStrategyTest
[ RUN ] COutputPopLimitStrategyTest.PositiveNos
[ OK ] COutputPopLimitStrategyTest.PositiveNos (0 ms)
[----------] 1 test from COutputPopLimitStrategyTest (1 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (1 ms total)
[ PASSED ] 1 test.

G++ 编译参数说明:

  • -std=c++11:指定 C++ 的版本
  • /usr/local/lib64/libgtest.so:链接 GoogleTest 的动态链接库
  • -lpthread:由于 GoogleTest 的内部使用了多线程,因此需要链接 pthread

Google Test 的使用案例

创建项目工程

下载代码

点击下载 完整的案例代码,项目的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
minder-gtest
├── CMakeLists.txt
├── include
│ └── datetime.h
├── src
│ ├── datetime.cpp
│ └── main.cpp
└── test
├── CMakeLists.txt
├── include
│ └── strUtil.h
└── src
└── main.cpp

编写项目代码

下载代码

这里的 C++ 代码,除了 main.cpp 的代码不一样之外,其他代码与上面的案例代码完全一致,不再累述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include "strUtil.h"
#include "datetime.h"
#include <gtest/gtest.h>

using namespace std;

// 去除字符串两边的空格
TEST(TestCase, test1) {
string str = " Hello World ! ";
trim(str);
ASSERT_EQ("Hello World !", str);
}

// 根据给定的日期,计算它是星期几
TEST(TestCase, test2) {
ASSERT_EQ(true, DateUtil::isWeekendDays("2022-01-09"));
}

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

CMake 配置文件

  • 主目录的 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
30
31
32
33
34
cmake_minimum_required(VERSION 3.15)

# 项目信息
project(minder)

# 定义C++的版本
set(CMAKE_CXX_STANDARD 11)

# 输出调试信息
set(CMAKE_CXX_FLAGS "-g")

# 开启所有警告
set(CMAKE_CXX_FLAGS "-Wall")

# 指定构建输出的目录
set(PROJECT_BINARY_DIR ${PROJECT_SOURCE_DIR}/build)

# 引入主项目的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索主项目的源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/src MAIN_SOURCES)

# 指定可执行文件的名称和主项目的所有源文件
add_executable(${PROJECT_NAME} ${MAIN_SOURCES})

# 启用单元测试
enable_testing()

# 添加子目录(子项目)
add_subdirectory(test)

# 添加单元测试的可执行文件
add_test(minder_test ${PROJECT_SOURCE_DIR}/test/build/minder_test)
  • test 目录的 CMakeLists.txt,这里的配置内容新增了 GoogleTest 库
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
cmake_minimum_required(VERSION 3.15)

# 项目信息
project(minder_test)

# 定义C++的版本
set(CMAKE_CXX_STANDARD 11)

# 查找 GoogleTest 库
find_package(GTest REQUIRED)

# 显示 GoogleTest 库的路径
MESSAGE(STATUS "GTEST_INCLUDE_DIRS : " ${GTEST_INCLUDE_DIRS})
MESSAGE(STATUS "GTEST_BOTH_LIBRARIES : " ${GTEST_BOTH_LIBRARIES})

# 搜索父目录(父项目)的头文件
include_directories(../include)

# 搜索父目录(父项目)的源文件
aux_source_directory(../src MAIN_SOURCES)

# 排除父目录(父项目)的入口源文件
list(FILTER MAIN_SOURCES EXCLUDE REGEX "main.cpp")

# 引入子项目的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索子项目里的源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/src TEST_SOURCES)

# 引入 GoogleTest 的头文件
include_directories(${GTEST_INCLUDE_DIRS})

# 指定构建输出的目录
set(PROJECT_BINARY_DIR ${PROJECT_SOURCE_DIR}/build)

# 指定可执行文件的名称和单元测试的所有源文件
add_executable(${PROJECT_NAME} ${MAIN_SOURCES} ${TEST_SOURCES})

# 链接 GoogleTest 与 pthread 库(请特别注意声明的顺序)
target_link_libraries(${PROJECT_NAME} ${GTEST_BOTH_LIBRARIES} pthread)

命令行编译项目

  • 编译 test 子项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 进入子项目的目录
$ cd minder-gtest/test

# 创建子项目的构建目录
$ mkdir build

# 进入子项目的构建目录
$ cd build

# 构建子项目
$ cmake ..

# 编译子项目
$ make

# 运行可执行文件
$ ./minder_test

运行可执行文件后,输出的日志信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from TestCase
[ RUN ] TestCase.test1
[ OK ] TestCase.test1 (0 ms)
[ RUN ] TestCase.test2
[ OK ] TestCase.test2 (0 ms)
[----------] 2 tests from TestCase (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (2 ms total)
[ PASSED ] 2 tests.
  • 编译主项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 进入主项目的目录
$ cd minder-gtest

# 创建主项目的构建目录
$ mkdir build

# 进入主项目的构建目录
$ cd build

# 构建主项目
$ cmake ..

# 编译主项目
$ make

# 执行项目测试
$ make test

# 运行可执行文件
$ ./minder

GoogleTest 使用扩展说明

在上面的案例中,GoogleTest 是使用源码编译的方式安装到 Linux 系统上的,这在迁移操作系统的时候,需要重复执行同样的安装步骤。此时为了方便日后迁移操作系统,可以将 GoogleTest 的头文件、动态链接都复制一份到项目中,这样就可以不依赖外部的系统环境了。

提示

点击下载 完整的案例代码,项目的目录结构如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
minder-gtest-plus
├── CMakeLists.txt
├── include
│ └── datetime.h
├── src
│ ├── datetime.cpp
│ └── main.cpp
├── test
│ ├── CMakeLists.txt
│ ├── include
│ │ └── strUtil.h
│ └── src
│ └── main.cpp
└── thirdparty
└── googletest
├── gmock
│ ├── include
│ │ └── gmock
│ │ ├── gmock-actions.h
│ │ ├── gmock-cardinalities.h
│ │ ├── gmock-function-mocker.h
│ │ ├── gmock-generated-actions.h
│ │ ├── gmock-generated-actions.h.pump
│ │ ├── gmock-generated-function-mockers.h
│ │ ├── gmock-generated-function-mockers.h.pump
│ │ ├── gmock-generated-matchers.h
│ │ ├── gmock-generated-matchers.h.pump
│ │ ├── gmock.h
│ │ ├── gmock-matchers.h
│ │ ├── gmock-more-actions.h
│ │ ├── gmock-more-matchers.h
│ │ ├── gmock-nice-strict.h
│ │ ├── gmock-spec-builders.h
│ │ └── internal
│ │ ├── custom
│ │ │ ├── gmock-generated-actions.h
│ │ │ ├── gmock-generated-actions.h.pump
│ │ │ ├── gmock-matchers.h
│ │ │ ├── gmock-port.h
│ │ │ └── README.md
│ │ ├── gmock-internal-utils.h
│ │ ├── gmock-port.h
│ │ └── gmock-pp.h
│ └── lib
│ ├── libgmock_main.so
│ └── libgmock.so
└── gtest
├── include
│ └── gtest
│ ├── gtest-death-test.h
│ ├── gtest.h
│ ├── gtest-matchers.h
│ ├── gtest-message.h
│ ├── gtest-param-test.h
│ ├── gtest_pred_impl.h
│ ├── gtest-printers.h
│ ├── gtest_prod.h
│ ├── gtest-spi.h
│ ├── gtest-test-part.h
│ ├── gtest-typed-test.h
│ └── internal
│ ├── custom
│ │ ├── gtest.h
│ │ ├── gtest-port.h
│ │ ├── gtest-printers.h
│ │ └── README.md
│ ├── gtest-death-test-internal.h
│ ├── gtest-filepath.h
│ ├── gtest-internal.h
│ ├── gtest-param-util.h
│ ├── gtest-port-arch.h
│ ├── gtest-port.h
│ ├── gtest-string.h
│ ├── gtest-type-util.h
│ └── gtest-type-util.h.pump
└── lib
├── libgtest_main.so
└── libgtest.so
  • test 目录的 CMakeLists.txt,这里的配置内容使用了项目里的 GoogleTest 库
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
cmake_minimum_required(VERSION 3.15)

# 定义 GoogleTest 库的目录路径
set(PATH_TO_GOOGLE_TEST ../thirdparty/googletest/gtest)
set(PATH_TO_GOOGLE_MOCK ../thirdparty/googletest/gmock)

# 项目信息
project(minder_test)

# 定义C++的版本
set(CMAKE_CXX_STANDARD 11)

# 搜索父目录(父项目)的头文件
include_directories(../include)

# 搜索父目录(父项目)的源文件
aux_source_directory(../src MAIN_SOURCES)

# 排除父目录(父项目)的入口源文件
list(FILTER MAIN_SOURCES EXCLUDE REGEX "main.cpp")

# 引入子项目的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索子项目里的源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/src TEST_SOURCES)

# 引入 GoogleTest 库的头文件
include_directories(${PATH_TO_GOOGLE_TEST}/include ${PATH_TO_GOOGLE_MOCK}/include)

# 指定 GoogleTest 动态链接库所在的目录
link_directories(${PATH_TO_GOOGLE_TEST}/lib ${PATH_TO_GOOGLE_MOCK}/lib)

# 指定构建输出的目录
set(PROJECT_BINARY_DIR ${PROJECT_SOURCE_DIR}/build)

# 指定可执行文件的名称和单元测试的所有源文件
add_executable(${PROJECT_NAME} ${MAIN_SOURCES} ${TEST_SOURCES})

# 链接 GoogleTest 与 pthread 库(请特别注意声明的顺序)
target_link_libraries(${PROJECT_NAME} gtest_main.so gtest.so gmock_main.so gmock.so pthread)
  • main.cpp 的 C++ 代码,与上面的案例代码完全一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include "strUtil.h"
#include "datetime.h"
#include <gtest/gtest.h>

using namespace std;

// 去除字符串两边的空格
TEST(TestCase, test1) {
string str = " Hello World ! ";
trim(str);
ASSERT_EQ("Hello World !", str);
}

// 根据给定的日期,计算它是星期几
TEST(TestCase, test2) {
ASSERT_EQ(true, DateUtil::isWeekendDays("2022-01-09"));
}

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

参考博客