第3章:项目初始化与测试目录结构

离线优先单元测试项目在初始化阶段必须建立一致的目录布局、统一的构建配置和可复现的依赖管理策略。本章从标准目录结构、CMake/配置文件和依赖锁定机制三个维度展开,并给出完整的启动命令序列。

3.1 标准目录布局(离线优先原则)

离线环境不允许在测试执行时联网下载依赖或插件,因此所有测试资源(源码、头文件、测试框架、模拟数据、配置文件)必须自包含于仓库内。推荐采用以下布局:

my_project/
├── CMakeLists.txt                # 顶层构建入口
├── cmake/
│   └── OfflineDeps.cmake         # 离线依赖查找脚本
├── src/
│   ├── CMakeLists.txt
│   ├── core/                     # 业务核心模块
│   ├── utils/                    # 工具函数
│   └── main.cpp
├── tests/
│   ├── CMakeLists.txt
│   ├── unit/                     # 纯单元测试(无外部依赖)
│   │   ├── test_core.cpp
│   │   └── test_utils.cpp
│   ├── integration/              # 集成测试(模拟真实环境)
│   │   └── test_pipeline.cpp
│   └── test_data/                # 测试用静态数据
│       ├── sample_input.bin
│       └── golden_output.bin
├── third_party/                  # 离线依赖(git子模块或vendor目录)
│   ├── catch2/
│   ├── googletest/
│   └── fmt/
├── scripts/
│   ├── setup_offline_env.sh      # 脚本:复制依赖到third_party
│   └── run_all_tests.sh          # 脚本:编译+运行所有测试
├── .gitignore
├── .clang-format                 # 代码格式统一
└── .cmake-format.yaml            # CMake格式化规则
目录/文件作用离线关键要求
tests/所有测试代码与数据测试可独立构建,不依赖主机已安装的库
third_party/第三方依赖源码/头文件通过 git submodule 或 vendor 目录锁定版本,禁止 URL 下载
test_data/测试输入与预期输出必须作为仓库文件,不生成临时文件污染构建目录
scripts/环境设置与运行脚本脚本内不应出现 curl、wget 等网络命令

原则:tests/ 目录下的所有 CMakeLists.txt 不应使用 FetchContent 或 find_package 的默认网络查找路径。必须显式指定 CMAKE_PREFIX_PATH 指向 third_party。

3.2 顶层 CMakeLists.txt 配置

构建系统必须支持离线运行。下面是一个最小配置示例,包含测试框架的静态链接:

cmake_minimum_required(VERSION 3.20)
project(MyOfflineProject VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ------------------------------------------------------------------
# 离线依赖路径(强制使用 third_party 目录,不联网)
# ------------------------------------------------------------------
list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/third_party")

# ------------------------------------------------------------------
# 单元测试子目录(仅在 BUILD_TESTING 开启时构建)
# ------------------------------------------------------------------
option(BUILD_TESTING "Build unit tests" ON)
if(BUILD_TESTING)
    enable_testing()
    add_subdirectory(tests)
endif()

# ------------------------------------------------------------------
# 可执行目标(示例)
# ------------------------------------------------------------------
add_subdirectory(src)

建议:始终将 BUILD_TESTING 设为默认开启,但允许用户通过 -DBUILD_TESTING=OFF 快速关闭,节省离线环境构建时间。

3.3 测试目录内的 CMakeLists.txt

每个测试子目录都应独立声明依赖,确保测试可被 ctest 单独发现:

# tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.20)

# 统一查找离线测试框架(以 Catch2 为例)
find_package(Catch2 REQUIRED)

# 单元测试:test_core
add_executable(test_core unit/test_core.cpp)
target_link_libraries(test_core PRIVATE MyCoreLib Catch2::Catch2WithMain)
target_include_directories(test_core PRIVATE ${CMAKE_SOURCE_DIR}/src)
add_test(NAME test_core COMMAND test_core)

# 集成测试:test_pipeline(需要额外数据文件)
add_executable(test_pipeline integration/test_pipeline.cpp)
target_link_libraries(test_pipeline PRIVATE MyPipelineLib Catch2::Catch2WithMain)
add_test(NAME test_pipeline COMMAND test_pipeline)
# 确保测试数据目录可被运行时访问
target_compile_definitions(test_pipeline PRIVATE
    TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/test_data")

常见错误:忘记 target_link_libraries 中的 Catch2WithMain 会导致链接失败;缺少 TEST_DATA_DIR 宏定义会导致集成测试运行时找不到数据文件。务必在每个测试目标上显式声明。

3.4 依赖管理:离线锁定与分发

使用 Git Submodule 或 Vendor 目录 锁定测试框架版本。以 Git Submodule 为例:

# 初始化 submodule(仅首次需要联网,之后离线)
git submodule add https://github.com/catchorg/Catch2.git third_party/catch2
git submodule update --init --recursive

# 后续开发者离线拉取 submodule(不需网络)
git submodule update --recursive

对于非 git 管理的依赖,可使用 setup_offline_env.sh 将预编译的静态库复制到 third_party/:

#!/bin/bash
# setup_offline_env.sh – 离线环境初始化脚本(无网络调用)
# 使用前需确保 vendor/ 目录中包含所有必需的头文件和库

VENDOR_DIR="$(dirname "$0")/../vendor"
THIRD_PARTY="$(dirname "$0")/../third_party"

echo "[OfflineEnv] Copying dependencies from vendor to third_party..."
cp -r "$VENDOR_DIR/catch2"/* "$THIRD_PARTY/catch2/"
cp -r "$VENDOR_DIR/fmt"/* "$THIRD_PARTY/fmt/"
echo "[OfflineEnv] Done."

离线优先的依赖管理要点:

  • 所有依赖必须存放在仓库内部(submodule 或 vendor),构建时禁止 fetch、download。
  • 使用 CMAKE_PREFIX_PATH 指向 third_party,不要依赖系统路径。
  • 测试框架建议采用 Header-Only 或静态链接,避免运行时动态库查找。

3.5 配置文件与代码格式化

离线环境仍需保证代码风格统一,推荐归档以下配置:

文件用途离线注意事项
.clang-formatC++代码格式化规则clang-format 必须安装在开发者本地,不依赖网络扩展
.cmake-format.yamlCMake 文件格式化使用 cmake-format 工具(Python包,可预先安装)
.gitignore忽略构建产物、IDE文件不要忽略 third_party/ 内的依赖,确保版本控制
.gitattributes文本/二进制文件区分PDF、测试数据等二进制文件设置 binary 属性

3.6 初始化流程与命令

在全新离线工作站上配置并运行测试的完整命令序列:

# 1. 克隆仓库(已包含 submodule 引用)
git clone https://gitlab.internal/my_project.git
cd my_project

# 2. 初始化 submodule(若仓库使用了 submodule)
git submodule update --init --recursive

# 3. 如果使用 vendor 目录,运行离线环境脚本
bash scripts/setup_offline_env.sh

# 4. 配置构建(指定离线依赖路径,禁止联网)
cmake -B build -DCMAKE_PREFIX_PATH="${PWD}/third_party" -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug

# 5. 构建并运行测试
cmake --build build --target all -j$(nproc)
cd build && ctest --output-on-failure -j$(nproc)

性能优化:使用 ccache 可以加速离线环境下的重复构建。在 CMakeLists.txt 顶部添加 set(CMAKE_CXX_COMPILER_LAUNCHER ccache) 即可。

3.7 本章结构图

项目初始化与测试目录结构总览 仓库根 (project/) 构建配置 源码 (src/) 测试 (tests/) 单元测试 (unit/) 集成测试 (integration/) 测试数据 (test_data/) third_party/ 依赖 CMakeLists.txt .clang-format / .gitignore 离线优先核心:目录自包含、依赖版本锁定、测试无需网络、可脱离 CI 执行

3.8 易错点与调试提示

  • 子模块未更新:克隆仓库后忘记 git submodule update --init --recursive,导致 third_party/catch2 为空目录,find_package 失败。运行 git submodule status 验证。
  • 路径权限:测试数据文件如果标记为可执行或不可读,测试将 I/O 失败。使用 git add --chmod=644 test_data/* 确保权限正确。
  • cmake 缓存残留:离线环境首次配置后更改了 CMAKE_PREFIX_PATH,需手动 rm -rf build 重新配置,否则旧路径仍生效。
  • 脚本换行符:Windows 开发者提交 setup_offline_env.sh 后,Linux 端执行时报错 bad interpreter。在 .gitattributes 中设置 *.sh text eol=lf 解决。

严重警告:不要在 CMakeLists.txt 中写入 include(FetchContent) 并指向网络 URL。即使开发机器有网络,离线工作站在隔离环境下构建会立即失败,且无法回退。应始终使用 third_party 本地路径。

3.9 小结