第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,是一条清晰的链路。
从图中可以看到,整个流程是线性的: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和覆盖率分析。