28、大型项目测试组织:测试分层、目录结构与依赖管理
说实话,我见过太多项目,测试代码写得一塌糊涂。要么是几百个测试文件堆在一个目录里,要么是单元测试和集成测试混在一起,跑起来谁都不敢动。嗯,今天我们就来聊聊,怎么把测试组织得像个样子。
一、测试为什么要分层?
我个人习惯把测试分成三层:单元测试、集成测试、系统测试。你想想看,这三层解决的问题完全不同。
| 测试层级 | 测试对象 | 典型工具 | 执行频率 |
|---|---|---|---|
| 单元测试 | 单个函数/类 | Google Test, Catch2 | 每次提交 |
| 集成测试 | 模块间接口 | CTest + 自定义脚本 | 每日构建 |
| 系统测试 | 完整功能链路 | CTest + 外部工具 | 发布前 |
我在项目中遇到过最典型的坑:有人把单元测试写成集成测试。一个测试用例里调了数据库、发了网络请求、还写了文件。结果一跑就是几分钟,根本没法在开发阶段频繁执行。说白了,单元测试就该快,毫秒级完成;集成测试可以慢一点,但也要控制在秒级。
核心原则:单元测试不依赖外部资源,集成测试可以依赖但必须可控,系统测试模拟真实环境。
二、测试目录结构怎么设计?
我推荐一种结构,用了好几年,团队反馈都不错。直接看例子:
project/
├── src/ # 源码
│ ├── module_a/
│ └── module_b/
├── tests/ # 测试根目录
│ ├── unit/ # 单元测试
│ │ ├── module_a/
│ │ │ ├── CMakeLists.txt
│ │ │ └── test_foo.cpp
│ │ └── module_b/
│ ├── integration/ # 集成测试
│ │ ├── test_api.cpp
│ │ └── test_db.cpp
│ ├── system/ # 系统测试
│ │ └── test_full_flow.py
│ ├── fixtures/ # 测试夹具
│ │ ├── test_data/
│ │ └── mock_servers/
│ └── CMakeLists.txt # 顶层测试配置
└── CMakeLists.txt
为什么要这么分?我解释一下:
- unit/ 目录下的测试,每个文件只测一个类或一个模块。编译快,跑得快。
- integration/ 目录放跨模块的测试。比如测试 A 模块调用 B 模块的接口是否正常。
- system/ 目录放端到端测试。我一般用 Python 写,因为灵活。
- fixtures/ 放共享的测试数据。避免每个测试文件都复制一份。
小技巧:单元测试的文件名用 test_<模块名>.cpp,集成测试用 test_<功能>.cpp。这样一眼就能看出测试范围。
三、测试依赖管理——最容易被忽视的坑
我曾经在一个项目里,因为测试依赖没管好,导致 CI 上测试结果不稳定。有时候能过,有时候不能过,查了半天发现是测试执行顺序的问题。
CMake 里管理测试依赖,主要靠这几个手段:
3.1 用 add_test 和 set_tests_properties
# 单元测试之间没有依赖
add_test(NAME test_module_a COMMAND test_module_a)
add_test(NAME test_module_b COMMAND test_module_b)
# 集成测试依赖单元测试通过
add_test(NAME test_integration COMMAND test_integration)
set_tests_properties(test_integration
PROPERTIES DEPENDS "test_module_a;test_module_b"
)
# 系统测试依赖集成测试
add_test(NAME test_system COMMAND test_system)
set_tests_properties(test_system
PROPERTIES DEPENDS "test_integration"
)
这样配置后,CTest 会自动按依赖顺序执行。如果单元测试失败,集成测试根本不会跑。节省时间,也避免误报。
3.2 资源锁——解决并行测试冲突
有些测试会占用同一个资源,比如数据库端口、文件锁。并行跑的时候就会打架。CMake 提供了 RESOURCE_LOCK 属性:
add_test(NAME test_db_write COMMAND test_db_write)
set_tests_properties(test_db_write
PROPERTIES RESOURCE_LOCK "db"
)
add_test(NAME test_db_read COMMAND test_db_read)
set_tests_properties(test_db_read
PROPERTIES RESOURCE_LOCK "db"
)
这样两个测试不会同时执行,但和其他不锁 db 的测试可以并行。嗯,这个功能很多人不知道,其实特别好用。
3.3 测试超时控制
我建议每个测试都设置超时。防止某个测试卡死,拖垮整个 CI 流程。
set_tests_properties(test_slow
PROPERTIES TIMEOUT 30
)
注意:超时时间不要设得太紧。单元测试一般 5-10 秒,集成测试 30-60 秒,系统测试可以到 5 分钟。留点余量,避免因为机器负载波动导致误报。
四、用 SVG 图看清测试分层与依赖
下面这张图,是我自己项目里实际用的测试分层结构。你可以看到测试是怎么一层层往上构建的。
五、CMakeLists.txt 实战配置
最后,给你看一个我实际项目里用的测试 CMakeLists.txt 配置。注意看怎么把三层测试组织在一起:
# tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
# 启用测试
enable_testing()
# 添加子目录
add_subdirectory(unit)
add_subdirectory(integration)
add_subdirectory(system)
# 设置测试超时全局默认值
set(CTEST_TEST_TIMEOUT 60 CACHE STRING "Default test timeout")
# 配置 CTest 并行
set(CTEST_PARALLEL_LEVEL 4)
然后在每个子目录里:
# tests/unit/CMakeLists.txt
# 编译单元测试
add_executable(test_module_a test_module_a.cpp)
target_link_libraries(test_module_a PRIVATE
gtest
gmock
module_a_lib
)
# 注册测试
add_test(NAME test_module_a COMMAND test_module_a)
set_tests_properties(test_module_a
PROPERTIES
TIMEOUT 10
LABELS "unit"
)
个人建议:给测试打上 LABELS 标签。比如 unit、integration、system。这样在 CI 上可以按标签筛选执行。比如提交代码时只跑 unit,每日构建跑全部。
我曾经踩过一个坑:集成测试里依赖了一个外部服务,但那个服务在 CI 环境里没启动。结果测试一直失败,排查了半天。后来我加了个 REQUIRED_FILES 检查,确保前置条件满足才跑测试。
# 检查外部服务是否可用
add_test(NAME check_db_service COMMAND check_db_available.sh)
set_tests_properties(check_db_service
PROPERTIES
FIXTURES_SETUP "db_fixture"
TIMEOUT 5
)
add_test(NAME test_integration_db COMMAND test_integration_db)
set_tests_properties(test_integration_db
PROPERTIES
FIXTURES_REQUIRED "db_fixture"
DEPENDS "test_module_a;test_module_b"
)
嗯,这样配置后,如果数据库没启动,check_db_service 会失败,后续的集成测试根本不会执行。避免了无意义的失败和排查时间。
总结一下:测试分层不是花架子,是实实在在帮你节省时间的手段。目录结构清晰了,依赖关系理清了,CI 上跑测试才敢放心。我见过太多项目,测试代码比源码还难维护,就是因为一开始没规划好。希望今天的分享对你有帮助。