第1章 测试与CTest:enable_testing()、add_test()、CTest配置、单元测试框架集成(Google Test)

大家好,我是你们的老朋友。今天咱们聊聊测试。说实话,我见过太多Android NDK项目,代码写得飞起,但一跑测试就抓瞎。很多人觉得测试是后端的事,跟底层开发没关系。嗯,这种想法我当年也有过,直到有一次我改了个底层内存分配的逻辑,结果把整个渲染管线搞崩了,查了两天才发现是单元测试没覆盖到。

从那天起,我就把测试当成了NDK开发的标配。今天这一章,咱们就系统地把CMake里的测试体系捋一遍。说白了,就是三件事:怎么启用测试、怎么添加测试用例、怎么跟Google Test这样的框架集成。

1.1 enable_testing():开启测试的大门

在CMake里,测试不是默认开启的。你得先告诉构建系统:“嘿,我要跑测试了!”这个动作就是enable_testing()

核心要点:enable_testing() 必须在顶级CMakeLists.txt中调用,且必须在 add_test() 之前。

我个人习惯把它放在项目配置的最前面,紧跟着 project() 之后。举个例子:

cmake_minimum_required(VERSION 3.10)
project(MyNDKProject VERSION 1.0 LANGUAGES C CXX)

# 开启测试支持
enable_testing()

# 后续添加子目录或库
add_subdirectory(src)
add_subdirectory(tests)

为什么必须放在顶级?因为CTest在运行时,会从顶级CMakeLists.txt开始递归查找测试目标。如果你在子目录里调用 enable_testing(),CTest根本不会理你。我在项目中遇到过有人把 enable_testing() 写在 tests/CMakeLists.txt 里,结果折腾了半天CTest就是找不到测试用例。

注意:如果你用 add_subdirectory() 引入了第三方库,那个库的CMakeLists.txt里如果也调用了 enable_testing(),可能会产生冲突。建议在顶级统一控制。

1.2 add_test():把测试用例注册进去

测试开关打开了,接下来就是往里面塞测试用例。add_test() 就是干这个的。它的基本语法很简单:

add_test(NAME <test_name> COMMAND <command> [<arg>...])

这里的 COMMAND 可以是任何可执行文件,不一定是CMake构建出来的。比如你可以直接跑一个Python脚本:

add_test(NAME my_python_test COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/test_script.py)

但更常见的用法,是配合 add_executable() 构建一个测试程序,然后把它注册为测试用例。比如:

# 在 tests/CMakeLists.txt 中
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE my_math_lib)

add_test(NAME test_math COMMAND test_math)

这里有个坑:add_test() 里的 COMMAND 参数,CMake会把它当作可执行文件的名字去查找。如果你没有设置 RUNTIME_OUTPUT_DIRECTORY,默认会生成在构建目录的对应子目录下。CTest在运行时,会自动把测试目标所在的构建目录加入PATH,所以一般不用操心路径问题。

但我曾经踩过一个坑:在Windows上,如果测试程序依赖某些DLL,而DLL不在PATH里,CTest会报“找不到DLL”的错误。解决办法是在 add_test() 之前设置环境变量:

set_tests_properties(test_math PROPERTIES ENVIRONMENT "PATH=${CMAKE_BINARY_DIR}/bin;$ENV{PATH}")

1.3 CTest配置:让测试跑得更顺手

CTest是CMake自带的测试驱动工具。你不需要额外安装,只要CMake装好了,CTest就在那里。但要想用好它,得了解几个关键配置。

1.3.1 CTestConfig.cmake 文件

这个文件放在项目的根目录,用来配置CTest的一些全局行为。比如设置测试超时时间、指定测试标签等。一个典型的配置:

# CTestConfig.cmake
set(CTEST_PROJECT_NAME "MyNDKProject")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")
set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=MyNDKProject")
set(CTEST_DROP_SITE_CDASH TRUE)

不过说实话,对于大多数Android NDK项目,我们不需要这么复杂的CDash集成。更常用的配置是设置测试超时和并行度:

# 在CMakeLists.txt中
set(CTEST_TEST_TIMEOUT 60)  # 每个测试最多跑60秒

然后在命令行跑测试时,用 -j 参数控制并行度:

ctest -j4  # 同时跑4个测试

1.3.2 测试标签与过滤

当测试用例多起来之后,你可能只想跑某一部分。CTest支持给测试打标签:

add_test(NAME test_math COMMAND test_math)
set_tests_properties(test_math PROPERTIES LABELS "unit;math")

add_test(NAME test_network COMMAND test_network)
set_tests_properties(test_network PROPERTIES LABELS "integration;network")

然后通过 -L-LE 过滤:

ctest -L unit      # 只跑unit标签的测试
ctest -LE network  # 排除network标签的测试

这个功能在大型项目中特别有用。我记得有一次,我们项目有300多个测试用例,每次全跑要半小时。后来按模块打了标签,开发时只跑自己模块的测试,效率提升了一大截。

1.4 集成Google Test:NDK项目的标配

Google Test(简称gtest)是C++社区最流行的单元测试框架之一。在Android NDK项目里集成它,其实比想象中简单。

1.4.1 获取Google Test源码

有两种方式:一是把源码直接放到项目里,二是用CMake的 FetchContent 自动下载。我个人推荐第二种,省心:

# 在顶级CMakeLists.txt中
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)

这样,gtest的库和目标就已经在你的构建系统里了。接下来就可以直接引用。

1.4.2 编写测试用例

一个典型的gtest测试文件长这样:

// test_math.cpp
#include <gtest/gtest.h>
#include "math_utils.h"

TEST(MathTest, Add) {
  EXPECT_EQ(add(1, 2), 3);
  EXPECT_EQ(add(-1, 1), 0);
}

TEST(MathTest, Multiply) {
  EXPECT_EQ(multiply(3, 4), 12);
  EXPECT_EQ(multiply(0, 5), 0);
}

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

1.4.3 构建并注册测试

tests/CMakeLists.txt 中:

# 添加测试可执行文件
add_executable(test_math test_math.cpp)

# 链接gtest和项目库
target_link_libraries(test_math PRIVATE
  gtest
  gtest_main
  my_math_lib
)

# 注册为CTest测试
add_test(NAME test_math COMMAND test_math)

这里有个细节:gtest_main 库提供了默认的 main() 函数,这样你就不用自己写了。如果你需要自定义main(比如初始化日志系统),那就只链接 gtest,自己写main。

小技巧:在Android NDK环境下,gtest默认使用 std::cout 输出结果。但在adb shell里,你可能看不到输出。建议在测试程序里加个 --gtest_print_time=0 参数,减少输出量,或者用 --gtest_output=xml:test_result.xml 把结果导出到文件。

1.5 实战:一个完整的NDK测试项目

说了这么多,咱们来个完整的例子。假设我们有一个简单的数学库,想给它加单元测试。

项目结构:

my_ndk_project/
├── CMakeLists.txt
├── src/
│   ├── CMakeLists.txt
│   └── math_utils.cpp
├── include/
│   └── math_utils.h
└── tests/
    ├── CMakeLists.txt
    └── test_math.cpp

顶级CMakeLists.txt:

cmake_minimum_required(VERSION 3.14)
project(MyNDKDemo VERSION 1.0 LANGUAGES CXX)

enable_testing()

# 获取gtest
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)

# 添加子目录
add_subdirectory(src)
add_subdirectory(tests)

src/CMakeLists.txt:

add_library(math_utils STATIC
  ${CMAKE_CURRENT_SOURCE_DIR}/math_utils.cpp
)
target_include_directories(math_utils PUBLIC
  ${CMAKE_SOURCE_DIR}/include
)

tests/CMakeLists.txt:

add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE
  gtest
  gtest_main
  math_utils
)

add_test(NAME test_math COMMAND test_math)

构建并运行:

mkdir build && cd build
cmake ..
make
ctest --output-on-failure

你会看到类似这样的输出:

Test project /path/to/build
    Start 1: test_math
1/1 Test #1: test_math ......................   Passed    0.01 sec

100% tests passed, 0 tests failed out of 1

1.6 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,从CMakeLists.txt到CTest,再到Google Test,是一条清晰的链路。

CMake测试体系知识结构图 CMakeLists.txt enable_testing() add_test(NAME ... COMMAND ...) CTest 运行测试 ctest -j4 -L unit Google Test 框架 TEST() / EXPECT_EQ() 最终目标:自动化、可重复的单元测试

从图中可以看到,整个流程是线性的:CMakeLists.txt 里先 enable_testing(),然后 add_test() 注册测试,最后CTest负责执行。而Google Test是测试框架的具体实现,它帮你写断言、组织测试用例。

1.7 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 坑一:在Android NDK交叉编译环境下,gtest的 FetchContent 可能会下载失败。解决办法是手动下载gtest源码,放到 third_party/ 目录下,然后用 add_subdirectory() 引入。
  • 坑二:CTest默认不会把测试输出打印到终端。如果你想知道测试失败的具体原因,记得加 --output-on-failure 参数。
  • 坑三:如果你的测试程序依赖动态库,在Android上运行时,记得用 adb shell 设置 LD_LIBRARY_PATH,或者把库推到 /system/lib64 下。

好了,这一章的内容就到这里。测试这件事,说白了就是给代码上保险。刚开始可能觉得麻烦,但一旦养成习惯,你会发现它帮你省下的调试时间,远比你写测试的时间多。下一章咱们聊聊更高级的测试技巧,包括mock和覆盖率分析。


公众号:蓝海资料掘金营,微信deep3321