活动公告

系统通知
05-18 21:22
系统通知
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

CMake与CMakeTest构建高效可靠的C++项目测试框架从基础配置到高级应用全面解析

SunJu_FaceMall

3万

主题

2860

科技点

3万

积分

白金月票

碾压王

积分
32872

塔罗立华奏

<font color=白金月票" /> 发表于 2025-9-3 01:30:01 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

在现代软件开发中,测试是保证代码质量和可靠性的关键环节。对于C++项目而言,构建一个高效、可靠的测试框架尤为重要。CMake作为一个跨平台的构建系统,配合CTest(CMake自带的测试工具,通常被称为CMakeTest)可以提供一个强大而灵活的测试解决方案。本文将从基础配置到高级应用,全面解析如何使用CMake和CTest构建高效可靠的C++项目测试框架。

CMake基础

CMake简介

CMake是一个开源、跨平台的构建自动化工具,它使用平台无关的配置文件(CMakeLists.txt)来生成标准的构建文件(如Unix的Makefile或Windows的Visual Studio项目)。CMake的主要优势在于其抽象了不同平台和编译器之间的差异,使开发者能够专注于代码本身而非构建细节。

CMake基本语法

CMake使用简单的命令语言,以下是一些基本语法元素:
  1. # 注释以#开头
  2. # 命令不区分大小写,但通常使用小写
  3. command(ARGS)
  4. # 变量设置
  5. set(VAR_NAME value)
  6. # 变量使用
  7. ${VAR_NAME}
  8. # 条件语句
  9. if(condition)
  10.     # commands
  11. elseif(condition2)
  12.     # commands
  13. else()
  14.     # commands
  15. endif()
  16. # 循环
  17. foreach(loop_var arg1 arg2 ...)
  18.     # commands
  19. endforeach()
复制代码

常用CMake命令

以下是一些常用的CMake命令:
  1. # 指定CMake最低版本要求
  2. cmake_minimum_required(VERSION 3.10)
  3. # 项目名称
  4. project(ProjectName)
  5. # 添加可执行文件
  6. add_executable(target_name source1.cpp source2.cpp)
  7. # 添加库
  8. add_library(library_name source1.cpp source2.cpp)
  9. # 包含目录
  10. include_directories(dir1 dir2)
  11. # 链接库
  12. target_link_libraries(target_name library_name)
  13. # 添加子目录
  14. add_subdirectory(dir_name)
复制代码

CTest简介

什么是CTest

CTest是CMake自带的测试工具,它提供了一个框架来管理和运行测试。CTest可以集成到CMake构建系统中,使得测试的配置和执行变得简单而高效。通过CTest,开发者可以轻松地定义测试用例、运行测试并收集测试结果。

为什么需要CTest

1. 集成度高:CTest与CMake无缝集成,无需额外配置。
2. 跨平台:与CMake一样,CTest可以在多个平台上运行。
3. 自动化测试:支持自动化测试流程,便于持续集成。
4. 丰富的报告:提供详细的测试报告,包括测试通过/失败状态、执行时间等。
5. 灵活的测试控制:支持并行执行、选择性运行测试等高级功能。

基础配置

安装CMake

在开始之前,确保已安装CMake。可以通过以下方式安装:

Ubuntu/Debian:
  1. sudo apt-get update
  2. sudo apt-get install cmake
复制代码

CentOS/RHEL:
  1. sudo yum install cmake
复制代码

macOS (使用Homebrew):
  1. brew install cmake
复制代码

Windows:从CMake官网下载安装程序并按照提示安装。

基本项目结构

一个典型的C++项目结构如下:
  1. my_project/
  2. ├── CMakeLists.txt
  3. ├── include/
  4. │   └── my_library/
  5. │       └── my_library.h
  6. ├── src/
  7. │   └── my_library.cpp
  8. └── tests/
  9.     ├── CMakeLists.txt
  10.     ├── test_my_library.cpp
  11.     └── main.cpp
复制代码

根目录CMakeLists.txt配置

项目根目录的CMakeLists.txt文件通常包含项目的基本配置:
  1. # 指定CMake最低版本要求
  2. cmake_minimum_required(VERSION 3.10)
  3. # 项目名称
  4. project(MyProject VERSION 1.0 LANGUAGES CXX)
  5. # 设置C++标准
  6. set(CMAKE_CXX_STANDARD 14)
  7. set(CMAKE_CXX_STANDARD_REQUIRED ON)
  8. # 添加库
  9. add_library(my_library
  10.     src/my_library.cpp
  11. )
  12. # 设置库的包含目录
  13. target_include_directories(my_library
  14.     PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
  15. )
  16. # 启用测试
  17. enable_testing()
  18. # 添加测试子目录
  19. add_subdirectory(tests)
复制代码

测试目录CMakeLists.txt配置

测试目录的CMakeLists.txt文件负责配置测试相关的设置:
  1. # 查找Google Test包
  2. find_package(GTest REQUIRED)
  3. # 添加测试可执行文件
  4. add_executable(test_runner
  5.     main.cpp
  6.     test_my_library.cpp
  7. )
  8. # 链接Google Test和项目库
  9. target_link_libraries(test_runner
  10.     PRIVATE GTest::gtest GTest::gtest_main
  11.     PRIVATE my_library
  12. )
  13. # 包含测试头文件
  14. target_include_directories(test_runner
  15.     PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include
  16. )
  17. # 添加测试
  18. add_test(NAME MyLibraryTest COMMAND test_runner)
复制代码

测试框架构建

选择测试框架

虽然CMake提供了CTest作为测试工具,但它本身不是一个测试框架。我们需要选择一个C++测试框架来编写测试用例。常见的选择包括:

1. Google Test (gtest):Google开发的C++测试框架,功能强大,社区活跃。
2. Catch2:一个现代的C++测试框架,仅头文件实现,易于集成。
3. Boost.Test:Boost库的一部分,功能全面。

本文以Google Test为例,因为它与CMake/CTest集成良好,且使用广泛。

安装Google Test

可以通过多种方式安装Google Test:

使用包管理器(Ubuntu/Debian):
  1. sudo apt-get install libgtest-dev
复制代码

使用vcpkg:
  1. vcpkg install gtest
复制代码

从源码构建:
  1. git clone https://github.com/google/googletest.git
  2. cd googletest
  3. mkdir build && cd build
  4. cmake ..
  5. make
  6. sudo make install
复制代码

编写测试用例

假设我们有一个简单的数学库,提供加法和减法功能:

include/my_library/math_utils.h:
  1. #ifndef MATH_UTILS_H
  2. #define MATH_UTILS_H
  3. namespace my_library {
  4. int add(int a, int b);
  5. int subtract(int a, int b);
  6. } // namespace my_library
  7. #endif // MATH_UTILS_H
复制代码

src/math_utils.cpp:
  1. #include "my_library/math_utils.h"
  2. namespace my_library {
  3. int add(int a, int b) {
  4.     return a + b;
  5. }
  6. int subtract(int a, int b) {
  7.     return a - b;
  8. }
  9. } // namespace my_library
复制代码

对应的测试用例如下:

tests/test_math_utils.cpp:
  1. #include <gtest/gtest.h>
  2. #include "my_library/math_utils.h"
  3. TEST(MathUtilsTest, Add) {
  4.     EXPECT_EQ(my_library::add(2, 3), 5);
  5.     EXPECT_EQ(my_library::add(-1, 1), 0);
  6.     EXPECT_EQ(my_library::add(-5, -7), -12);
  7. }
  8. TEST(MathUtilsTest, Subtract) {
  9.     EXPECT_EQ(my_library::subtract(5, 3), 2);
  10.     EXPECT_EQ(my_library::subtract(1, 1), 0);
  11.     EXPECT_EQ(my_library::subtract(-5, -7), 2);
  12. }
复制代码

tests/main.cpp:
  1. #include <gtest/gtest.h>
  2. int main(int argc, char **argv) {
  3.     ::testing::InitGoogleTest(&argc, argv);
  4.     return RUN_ALL_TESTS();
  5. }
复制代码

更新CMakeLists.txt

更新根目录和测试目录的CMakeLists.txt文件以包含数学库:

CMakeLists.txt:
  1. cmake_minimum_required(VERSION 3.10)
  2. project(MyProject VERSION 1.0 LANGUAGES CXX)
  3. set(CMAKE_CXX_STANDARD 14)
  4. set(CMAKE_CXX_STANDARD_REQUIRED ON)
  5. add_library(my_library
  6.     src/math_utils.cpp
  7. )
  8. target_include_directories(my_library
  9.     PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
  10. )
  11. enable_testing()
  12. add_subdirectory(tests)
复制代码

tests/CMakeLists.txt:
  1. find_package(GTest REQUIRED)
  2. add_executable(test_runner
  3.     main.cpp
  4.     test_math_utils.cpp
  5. )
  6. target_link_libraries(test_runner
  7.     PRIVATE GTest::gtest GTest::gtest_main
  8.     PRIVATE my_library
  9. )
  10. target_include_directories(test_runner
  11.     PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include
  12. )
  13. add_test(NAME MathUtilsTest COMMAND test_runner)
复制代码

构建和运行测试

现在可以构建项目并运行测试:
  1. # 创建构建目录
  2. mkdir build && cd build
  3. # 配置项目
  4. cmake ..
  5. # 构建项目
  6. make
  7. # 运行测试
  8. ctest
复制代码

或者直接运行测试可执行文件:
  1. ./tests/test_runner
复制代码

高级应用

测试覆盖率

测试覆盖率是衡量测试质量的重要指标。CMake可以集成gcov/lcov等工具来生成覆盖率报告。

首先,确保安装了gcov和lcov:

Ubuntu/Debian:
  1. sudo apt-get install gcov lcov
复制代码

然后,更新CMakeLists.txt以启用覆盖率支持:

CMakeLists.txt (添加以下内容):
  1. option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
  2. if(ENABLE_COVERAGE)
  3.     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
  4.     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
  5. endif()
复制代码

tests/CMakeLists.txt (添加以下内容):
  1. if(ENABLE_COVERAGE)
  2.     add_custom_target(coverage
  3.         COMMAND lcov --directory . --capture --output-file coverage.info
  4.         COMMAND lcov --remove coverage.info '/usr/*' '${CMAKE_CURRENT_SOURCE_DIR}/tests/*' --output-file coverage.info.cleaned
  5.         COMMAND genhtml coverage.info.cleaned --output-directory coverage
  6.         WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
  7.         COMMENT "Generating code coverage report"
  8.     )
  9. endif()
复制代码

构建并生成覆盖率报告:
  1. mkdir build && cd build
  2. cmake .. -DENABLE_COVERAGE=ON
  3. make
  4. ctest
  5. make coverage
复制代码

覆盖率报告将生成在build/coverage目录中。

并行测试

对于大型项目,测试可能需要很长时间才能完成。CTest支持并行执行测试以减少总执行时间。
  1. # 使用4个并行作业运行测试
  2. ctest -j 4
复制代码

测试分类和过滤

可以使用标签对测试进行分类,并选择性地运行特定类别的测试。

tests/test_math_utils.cpp (修改为):
  1. #include <gtest/gtest.h>
  2. #include "my_library/math_utils.h"
  3. TEST(MathUtilsTest, Add) {
  4.     EXPECT_EQ(my_library::add(2, 3), 5);
  5.     EXPECT_EQ(my_library::add(-1, 1), 0);
  6.     EXPECT_EQ(my_library::add(-5, -7), -12);
  7. }
  8. TEST(MathUtilsTest, Subtract) {
  9.     EXPECT_EQ(my_library::subtract(5, 3), 2);
  10.     EXPECT_EQ(my_library::subtract(1, 1), 0);
  11.     EXPECT_EQ(my_library::subtract(-5, -7), 2);
  12. }
  13. // 标记测试
  14. TEST(MathUtilsTest, ComplexAdd, .testing) {
  15.     EXPECT_EQ(my_library::add(1000000, 2000000), 3000000);
  16. }
复制代码

运行特定标签的测试:
  1. # 只运行标记为"testing"的测试
  2. ctest -R testing
复制代码

内存检查

CTest可以集成内存检查工具,如Valgrind,以检测内存泄漏和其他内存问题。

首先,确保安装了Valgrind:

Ubuntu/Debian:
  1. sudo apt-get install valgrind
复制代码

然后,运行内存检查:
  1. # 配置CTest使用Valgrind
  2. ctest -T memcheck
复制代码

持续集成

将CMake/CTest集成到持续集成(CI)系统中可以自动化测试流程。以下是GitHub Actions的示例配置:

.github/workflows/ci.yml:
  1. name: CI
  2. on: [push, pull_request]
  3. jobs:
  4.   build-and-test:
  5.     runs-on: ubuntu-latest
  6.    
  7.     steps:
  8.     - uses: actions/checkout@v2
  9.    
  10.     - name: Install dependencies
  11.       run: |
  12.         sudo apt-get update
  13.         sudo apt-get install cmake libgtest-dev lcov
  14.    
  15.     - name: Configure
  16.       run: cmake -S . -B build -DENABLE_COVERAGE=ON
  17.    
  18.     - name: Build
  19.       run: cmake --build build
  20.    
  21.     - name: Test
  22.       run: cd build && ctest --output-on-failure
  23.    
  24.     - name: Generate coverage
  25.       run: cd build && make coverage
  26.    
  27.     - name: Upload coverage to Codecov
  28.       uses: codecov/codecov-action@v1
  29.       with:
  30.         file: ./build/coverage.info.cleaned
复制代码

最佳实践

项目结构组织

良好的项目结构有助于维护和扩展测试框架。推荐以下结构:
  1. my_project/
  2. ├── CMakeLists.txt
  3. ├── include/
  4. ├── src/
  5. ├── tests/
  6. │   ├── CMakeLists.txt
  7. │   ├── unit/          # 单元测试
  8. │   ├── integration/   # 集成测试
  9. │   ├── performance/   # 性能测试
  10. │   └── main.cpp       # 测试主程序
  11. ├── cmake/             # 自定义CMake模块
  12. ├── docs/              # 文档
  13. └── examples/          # 示例代码
复制代码

测试命名约定

一致的测试命名约定可以提高可读性和可维护性:
  1. // 格式: TestSuiteName_TestName_ExpectedBehavior
  2. TEST(MathUtilsTest, Add_TwoPositiveNumbers_ReturnsCorrectSum) {
  3.     EXPECT_EQ(my_library::add(2, 3), 5);
  4. }
  5. TEST(MathUtilsTest, Add_PositiveAndNegativeNumbers_ReturnsCorrectSum) {
  6.     EXPECT_EQ(my_library::add(-1, 1), 0);
  7. }
复制代码

测试夹具(Fixtures)

对于需要共享设置的测试,使用Google Test的测试夹具:
  1. class MathUtilsTest : public ::testing::Test {
  2. protected:
  3.     void SetUp() override {
  4.         // 测试前的设置
  5.     }
  6.     void TearDown() override {
  7.         // 测试后的清理
  8.     }
  9.     my_library::Calculator calc;  // 共享对象
  10. };
  11. TEST_F(MathUtilsTest, Add) {
  12.     EXPECT_EQ(calc.add(2, 3), 5);
  13. }
  14. TEST_F(MathUtilsTest, Subtract) {
  15.     EXPECT_EQ(calc.subtract(5, 3), 2);
  16. }
复制代码

参数化测试

对于需要使用多组输入测试相同逻辑的情况,使用参数化测试:
  1. class MathUtilsAddTest : public ::testing::TestWithParam<std::pair<int, int>> {
  2. protected:
  3.     my_library::Calculator calc;
  4. };
  5. TEST_P(MathUtilsAddTest, AddReturnsCorrectSum) {
  6.     auto params = GetParam();
  7.     int expected = params.first + params.second;
  8.     EXPECT_EQ(calc.add(params.first, params.second), expected);
  9. }
  10. INSTANTIATE_TEST_SUITE_P(
  11.     PositiveNumbers,
  12.     MathUtilsAddTest,
  13.     ::testing::Values(
  14.         std::make_pair(1, 2),
  15.         std::make_pair(5, 7),
  16.         std::make_pair(10, 20)
  17.     )
  18. );
  19. INSTANTIATE_TEST_SUITE_P(
  20.     NegativeNumbers,
  21.     MathUtilsAddTest,
  22.     ::testing::Values(
  23.         std::make_pair(-1, -2),
  24.         std::make_pair(-5, -7),
  25.         std::make_pair(-10, -20)
  26.     )
  27. );
复制代码

Mock对象

对于依赖外部组件的测试,使用Google Mock创建模拟对象:

首先,安装Google Mock(通常与Google Test一起安装)。
  1. #include <gmock/gmock.h>
  2. // 模拟接口
  3. class MockDatabase {
  4. public:
  5.     MOCK_METHOD(int, getValue, (int key), (const));
  6. };
  7. // 使用模拟对象的测试
  8. TEST(DatabaseTest, GetValue) {
  9.     MockDatabase mockDb;
  10.    
  11.     // 设置期望
  12.     EXPECT_CALL(mockDb, getValue(42))
  13.         .WillOnce(Return(100));
  14.    
  15.     // 测试代码
  16.     int result = mockDb.getValue(42);
  17.     EXPECT_EQ(result, 100);
  18. }
复制代码

CMake函数和宏

为了简化CMakeLists.txt并提高可重用性,定义自定义函数和宏:

cmake/AddTests.cmake:
  1. # 添加测试的函数
  2. function(add_tests test_name sources)
  3.     add_executable(${test_name}_runner ${sources})
  4.     target_link_libraries(${test_name}_runner
  5.         PRIVATE GTest::gtest GTest::gtest_main
  6.         PRIVATE my_library
  7.     )
  8.     target_include_directories(${test_name}_runner
  9.         PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include
  10.     )
  11.     add_test(NAME ${test_name} COMMAND ${test_name}_runner)
  12. endfunction()
  13. # 添加带有覆盖率的测试的宏
  14. macro(add_test_with_coverage test_name sources)
  15.     add_tests(${test_name} ${sources})
  16.    
  17.     if(ENABLE_COVERAGE)
  18.         # 添加覆盖率目标
  19.         set_target_properties(${test_name}_runner PROPERTIES
  20.             COMPILE_FLAGS "--coverage"
  21.             LINK_FLAGS "--coverage"
  22.         )
  23.     endif()
  24. endmacro()
复制代码

然后在tests/CMakeLists.txt中使用这些函数:
  1. # 包含自定义CMake模块
  2. list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../cmake)
  3. include(AddTests)
  4. # 使用自定义函数添加测试
  5. add_tests(MathUtilsTest
  6.     main.cpp
  7.     test_math_utils.cpp
  8. )
  9. # 使用自定义宏添加带覆盖率的测试
  10. add_test_with_coverage(CalculatorTest
  11.     test_calculator.cpp
  12. )
复制代码

案例分析

让我们通过一个更复杂的例子来展示如何使用CMake和CTest构建一个完整的测试框架。假设我们正在开发一个简单的JSON解析器库。

项目结构
  1. json_parser/
  2. ├── CMakeLists.txt
  3. ├── include/
  4. │   └── json_parser/
  5. │       ├── json_parser.h
  6. │       ├── json_value.h
  7. │       └── json_exception.h
  8. ├── src/
  9. │   ├── json_parser.cpp
  10. │   ├── json_value.cpp
  11. │   └── json_exception.cpp
  12. ├── tests/
  13. │   ├── CMakeLists.txt
  14. │   ├── main.cpp
  15. │   ├── unit/
  16. │   │   ├── CMakeLists.txt
  17. │   │   ├── test_json_parser.cpp
  18. │   │   └── test_json_value.cpp
  19. │   ├── integration/
  20. │   │   ├── CMakeLists.txt
  21. │   │   └── test_json_integration.cpp
  22. │   └── performance/
  23. │       ├── CMakeLists.txt
  24. │       └── test_json_performance.cpp
  25. ├── cmake/
  26. │   └── AddTests.cmake
  27. └── examples/
  28.     └── simple_example.cpp
复制代码

根目录CMakeLists.txt
  1. cmake_minimum_required(VERSION 3.10)
  2. project(JsonParser VERSION 1.0 LANGUAGES CXX)
  3. set(CMAKE_CXX_STANDARD 17)
  4. set(CMAKE_CXX_STANDARD_REQUIRED ON)
  5. # 添加库
  6. add_library(json_parser
  7.     src/json_parser.cpp
  8.     src/json_value.cpp
  9.     src/json_exception.cpp
  10. )
  11. # 设置库的包含目录
  12. target_include_directories(json_parser
  13.     PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
  14. )
  15. # 启用测试
  16. enable_testing()
  17. # 添加测试子目录
  18. add_subdirectory(tests)
  19. # 添加示例
  20. add_subdirectory(examples)
  21. # 安装规则
  22. install(TARGETS json_parser
  23.     EXPORT JsonParserTargets
  24.     LIBRARY DESTINATION lib
  25.     ARCHIVE DESTINATION lib
  26.     RUNTIME DESTINATION bin
  27. )
  28. install(DIRECTORY include/json_parser
  29.     DESTINATION include
  30. )
  31. install(EXPORT JsonParserTargets
  32.     FILE JsonParserTargets.cmake
  33.     DESTINATION lib/cmake/JsonParser
  34. )
  35. # 包含配置文件
  36. include(CMakePackageConfigHelpers)
  37. write_basic_package_version_file(
  38.     "${CMAKE_CURRENT_BINARY_DIR}/JsonParserConfigVersion.cmake"
  39.     VERSION ${JsonParser_VERSION}
  40.     COMPATIBILITY AnyNewerVersion
  41. )
  42. install(FILES "${CMAKE_CURRENT_BINARY_DIR}/JsonParserConfigVersion.cmake"
  43.     DESTINATION lib/cmake/JsonParser
  44. )
复制代码

cmake/AddTests.cmake
  1. # 添加测试的函数
  2. function(add_tests test_name sources)
  3.     add_executable(${test_name}_runner ${sources})
  4.     target_link_libraries(${test_name}_runner
  5.         PRIVATE GTest::gtest GTest::gtest_main
  6.         PRIVATE json_parser
  7.     )
  8.     target_include_directories(${test_name}_runner
  9.         PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../include
  10.     )
  11.     add_test(NAME ${test_name} COMMAND ${test_name}_runner)
  12. endfunction()
  13. # 添加带有覆盖率的测试的宏
  14. macro(add_test_with_coverage test_name sources)
  15.     add_tests(${test_name} ${sources})
  16.    
  17.     if(ENABLE_COVERAGE)
  18.         # 添加覆盖率目标
  19.         set_target_properties(${test_name}_runner PROPERTIES
  20.             COMPILE_FLAGS "--coverage"
  21.             LINK_FLAGS "--coverage"
  22.         )
  23.     endif()
  24. endmacro()
复制代码

tests/CMakeLists.txt
  1. # 查找Google Test包
  2. find_package(GTest REQUIRED)
  3. # 包含自定义CMake模块
  4. list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../cmake)
  5. include(AddTests)
  6. # 添加测试主程序
  7. add_executable(test_runner main.cpp)
  8. target_link_libraries(test_runner
  9.     PRIVATE GTest::gtest GTest::gtest_main
  10.     PRIVATE json_parser
  11. )
  12. target_include_directories(test_runner
  13.     PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include
  14. )
  15. # 添加测试子目录
  16. add_subdirectory(unit)
  17. add_subdirectory(integration)
  18. add_subdirectory(performance)
  19. # 添加测试
  20. add_test(NAME JsonParserTest COMMAND test_runner)
  21. # 覆盖率配置
  22. option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
  23. if(ENABLE_COVERAGE)
  24.     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
  25.     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
  26.    
  27.     add_custom_target(coverage
  28.         COMMAND lcov --directory . --capture --output-file coverage.info
  29.         COMMAND lcov --remove coverage.info '/usr/*' '${CMAKE_CURRENT_SOURCE_DIR}/tests/*' --output-file coverage.info.cleaned
  30.         COMMAND genhtml coverage.info.cleaned --output-dir coverage
  31.         WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
  32.         COMMENT "Generating code coverage report"
  33.     )
  34. endif()
复制代码

tests/unit/test_json_parser.cpp
  1. #include <gtest/gtest.h>
  2. #include "json_parser/json_parser.h"
  3. #include "json_parser/json_exception.h"
  4. TEST(JsonParserTest, ParseEmptyObject) {
  5.     json_parser::JsonParser parser;
  6.     json_parser::JsonValue value = parser.parse("{}");
  7.    
  8.     EXPECT_TRUE(value.isObject());
  9.     EXPECT_EQ(value.getObject().size(), 0);
  10. }
  11. TEST(JsonParserTest, ParseSimpleKeyValue) {
  12.     json_parser::JsonParser parser;
  13.     json_parser::JsonValue value = parser.parse("{"key": "value"}");
  14.    
  15.     EXPECT_TRUE(value.isObject());
  16.     auto obj = value.getObject();
  17.     EXPECT_EQ(obj.size(), 1);
  18.     EXPECT_TRUE(obj.find("key") != obj.end());
  19.     EXPECT_TRUE(obj["key"].isString());
  20.     EXPECT_EQ(obj["key"].getString(), "value");
  21. }
  22. TEST(JsonParserTest, ParseInvalidJsonThrowsException) {
  23.     json_parser::JsonParser parser;
  24.    
  25.     EXPECT_THROW(parser.parse("{invalid json}"), json_parser::JsonParseException);
  26. }
  27. TEST(JsonParserTest, ParseNestedObject) {
  28.     json_parser::JsonParser parser;
  29.     json_parser::JsonValue value = parser.parse("{"outer": {"inner": 42}}");
  30.    
  31.     EXPECT_TRUE(value.isObject());
  32.     auto obj = value.getObject();
  33.     EXPECT_EQ(obj.size(), 1);
  34.     EXPECT_TRUE(obj.find("outer") != obj.end());
  35.     EXPECT_TRUE(obj["outer"].isObject());
  36.    
  37.     auto innerObj = obj["outer"].getObject();
  38.     EXPECT_EQ(innerObj.size(), 1);
  39.     EXPECT_TRUE(innerObj.find("inner") != innerObj.end());
  40.     EXPECT_TRUE(innerObj["inner"].isNumber());
  41.     EXPECT_EQ(innerObj["inner"].getNumber(), 42);
  42. }
复制代码

tests/integration/test_json_integration.cpp
  1. #include <gtest/gtest.h>
  2. #include "json_parser/json_parser.h"
  3. #include <fstream>
  4. #include <filesystem>
  5. TEST(JsonParserIntegrationTest, ParseRealWorldJsonFile) {
  6.     // 创建一个临时JSON文件
  7.     std::string tempFile = "temp_test.json";
  8.     std::ofstream file(tempFile);
  9.     file << R"({
  10.         "name": "John Doe",
  11.         "age": 30,
  12.         "isStudent": false,
  13.         "address": {
  14.             "street": "123 Main St",
  15.             "city": "Anytown"
  16.         },
  17.         "hobbies": ["reading", "swimming", "coding"]
  18.     })";
  19.     file.close();
  20.    
  21.     // 解析JSON文件
  22.     json_parser::JsonParser parser;
  23.     json_parser::JsonValue value = parser.parseFile(tempFile);
  24.    
  25.     // 验证解析结果
  26.     EXPECT_TRUE(value.isObject());
  27.     auto obj = value.getObject();
  28.    
  29.     EXPECT_TRUE(obj.find("name") != obj.end());
  30.     EXPECT_TRUE(obj["name"].isString());
  31.     EXPECT_EQ(obj["name"].getString(), "John Doe");
  32.    
  33.     EXPECT_TRUE(obj.find("age") != obj.end());
  34.     EXPECT_TRUE(obj["age"].isNumber());
  35.     EXPECT_EQ(obj["age"].getNumber(), 30);
  36.    
  37.     EXPECT_TRUE(obj.find("isStudent") != obj.end());
  38.     EXPECT_TRUE(obj["isStudent"].isBoolean());
  39.     EXPECT_EQ(obj["isStudent"].getBoolean(), false);
  40.    
  41.     EXPECT_TRUE(obj.find("address") != obj.end());
  42.     EXPECT_TRUE(obj["address"].isObject());
  43.     auto address = obj["address"].getObject();
  44.     EXPECT_EQ(address["street"].getString(), "123 Main St");
  45.     EXPECT_EQ(address["city"].getString(), "Anytown");
  46.    
  47.     EXPECT_TRUE(obj.find("hobbies") != obj.end());
  48.     EXPECT_TRUE(obj["hobbies"].isArray());
  49.     auto hobbies = obj["hobbies"].getArray();
  50.     EXPECT_EQ(hobbies.size(), 3);
  51.     EXPECT_EQ(hobbies[0].getString(), "reading");
  52.     EXPECT_EQ(hobbies[1].getString(), "swimming");
  53.     EXPECT_EQ(hobbies[2].getString(), "coding");
  54.    
  55.     // 清理临时文件
  56.     std::filesystem::remove(tempFile);
  57. }
复制代码

tests/performance/test_json_performance.cpp
  1. #include <gtest/gtest.h>
  2. #include "json_parser/json_parser.h"
  3. #include <chrono>
  4. #include <fstream>
  5. #include <filesystem>
  6. TEST(JsonParserPerformanceTest, ParseLargeJson) {
  7.     // 创建一个大型JSON文件
  8.     std::string tempFile = "large_test.json";
  9.     std::ofstream file(tempFile);
  10.    
  11.     file << R"({"array": [)";
  12.     for (int i = 0; i < 10000; ++i) {
  13.         if (i > 0) file << ",";
  14.         file << R"({"id": )" << i << R"(, "value": "item)" << i << R"("})";
  15.     }
  16.     file << R"(]})";
  17.     file.close();
  18.    
  19.     // 测量解析时间
  20.     json_parser::JsonParser parser;
  21.     auto start = std::chrono::high_resolution_clock::now();
  22.     json_parser::JsonValue value = parser.parseFile(tempFile);
  23.     auto end = std::chrono::high_resolution_clock::now();
  24.    
  25.     auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
  26.     std::cout << "Parsing large JSON took " << duration.count() << " ms\n";
  27.    
  28.     // 验证解析结果
  29.     EXPECT_TRUE(value.isObject());
  30.     auto obj = value.getObject();
  31.     EXPECT_TRUE(obj.find("array") != obj.end());
  32.     EXPECT_TRUE(obj["array"].isArray());
  33.     auto array = obj["array"].getArray();
  34.     EXPECT_EQ(array.size(), 10000);
  35.    
  36.     // 验证第一个和最后一个元素
  37.     EXPECT_TRUE(array[0].isObject());
  38.     auto firstItem = array[0].getObject();
  39.     EXPECT_EQ(firstItem["id"].getNumber(), 0);
  40.     EXPECT_EQ(firstItem["value"].getString(), "item0");
  41.    
  42.     EXPECT_TRUE(array[9999].isObject());
  43.     auto lastItem = array[9999].getObject();
  44.     EXPECT_EQ(lastItem["id"].getNumber(), 9999);
  45.     EXPECT_EQ(lastItem["value"].getString(), "item9999");
  46.    
  47.     // 清理临时文件
  48.     std::filesystem::remove(tempFile);
  49. }
复制代码

构建和运行测试
  1. # 创建构建目录
  2. mkdir build && cd build
  3. # 配置项目(启用覆盖率)
  4. cmake .. -DENABLE_COVERAGE=ON
  5. # 构建项目
  6. make
  7. # 运行所有测试
  8. ctest
  9. # 运行特定类别的测试
  10. ctest -R JsonParserUnitTest
  11. ctest -R JsonParserIntegrationTest
  12. ctest -R JsonParserPerformanceTest
  13. # 生成覆盖率报告
  14. make coverage
复制代码

总结与展望

本文详细介绍了如何使用CMake和CTest构建高效可靠的C++项目测试框架。我们从基础配置开始,逐步介绍了CMake和CTest的基本概念、测试框架的构建、高级应用以及最佳实践。通过一个完整的JSON解析器案例,展示了如何组织项目结构、编写不同类型的测试以及集成到持续集成系统中。

CMake和CTest的组合为C++项目提供了一个强大而灵活的测试解决方案。它们不仅支持单元测试,还支持集成测试、性能测试等多种测试类型。通过合理配置,还可以实现测试覆盖率分析、内存检查等高级功能。

随着软件开发实践的不断演进,测试框架也在不断发展。未来,我们可以期待以下方面的改进:

1. 更好的集成:CMake和CTest与更多测试框架和工具的集成。
2. 更丰富的报告:更详细、更直观的测试报告和分析。
3. 更智能的测试选择:基于代码变更自动选择相关测试,提高测试效率。
4. 云测试支持:更好地支持分布式测试和云端测试资源。

通过掌握CMake和CTest的使用,开发者可以构建一个高效、可靠的测试框架,为C++项目的质量保驾护航。希望本文能够帮助读者更好地理解和应用这些工具,提高软件开发的质量和效率。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则