跳至内容

测试、基准测试与消毒器

使用 GoogleTest 编写单元测试、使用 Google Benchmark 进行基准测试、使用消毒器(ASan、UBSan、TSan)捕获 bug,并将静态分析集成到你的 CI/CD 管道中。

C++ 的测试金字塔

专业的 C++ 开发依赖多层质量保障:单元测试验证单个函数和类,集成测试检查组件交互,消毒器在运行时捕获内存错误和未定义行为,静态分析在不运行代码的情况下发现 bug,基准测试确保性能满足要求。这些工具共同构成一个安全网,在 bug 到达生产环境之前将其捕获。

两个最流行的 C++ 测试框架是 GoogleTest(来自 Google,在行业中广泛使用)和 Catch2(仅头文件,BDD 风格语法)。两者都能与 CMake 和 CI/CD 系统良好集成。

使用 GoogleTest 进行单元测试

GoogleTest 提供断言宏(EXPECT_* 用于非致命断言,ASSERT_* 用于致命断言)、用于共享设置/清理的测试夹具,以及参数化测试:

test_factorial.cpp
#include <gtest/gtest.h>
#include <vector>
#include <stdexcept>

// Function under test
int factorial(int n) {
    if (n < 0) throw std::invalid_argument("negative input");
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// Basic test — TEST(TestSuite, TestName)
TEST(FactorialTest, HandlesZero) {
    EXPECT_EQ(factorial(0), 1);
}

TEST(FactorialTest, HandlesPositive) {
    EXPECT_EQ(factorial(1), 1);
    EXPECT_EQ(factorial(5), 120);
    EXPECT_EQ(factorial(10), 3628800);
}

TEST(FactorialTest, ThrowsOnNegative) {
    EXPECT_THROW(factorial(-1), std::invalid_argument);
}

// Test fixture — shared setup/teardown
class StackTest : public ::testing::Test {
protected:
    std::vector<int> stack;

    void SetUp() override {
        stack.push_back(1);
        stack.push_back(2);
        stack.push_back(3);
    }

    void TearDown() override {
        stack.clear();
    }
};

TEST_F(StackTest, TopElement) {
    EXPECT_EQ(stack.back(), 3);
}

TEST_F(StackTest, PopReducesSize) {
    stack.pop_back();
    EXPECT_EQ(stack.size(), 2);
    EXPECT_EQ(stack.back(), 2);
}

TEST_F(StackTest, IsNotEmpty) {
    ASSERT_FALSE(stack.empty());  // fatal — stops test if false
    EXPECT_GT(stack.size(), 0u);  // non-fatal — continues on failure
}

将测试与 CMake 集成

使用 CMake 设置 GoogleTest 并通过 CTest(内置测试运行器)运行测试:

CMakeLists.txt
# CMakeLists.txt for testing
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Fetch GoogleTest
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

# Main library
add_library(mylib src/math.cpp src/utils.cpp)
target_include_directories(mylib PUBLIC include)

# Test executable
enable_testing()
add_executable(unit_tests
    tests/test_math.cpp
    tests/test_utils.cpp
)
target_link_libraries(unit_tests PRIVATE mylib GTest::gtest_main)

# Register tests with CTest
include(GoogleTest)
gtest_discover_tests(unit_tests)

# Run tests:
# cmake -B build && cmake --build build
# cd build && ctest --output-on-failure --parallel $(nproc)

消毒器:运行时 Bug 检测

消毒器是编译器插桩的运行时检查,可以捕获整类 bug。通过编译器标志启用它们 ── 它们增加约 2 倍的性能开销,但能捕获没有它们再多测试也无法发现的 bug:

terminal
# AddressSanitizer (ASan) — catches memory errors
# Buffer overflows, use-after-free, double-free, memory leaks
cmake -B build-asan \
    -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"

# UndefinedBehaviorSanitizer (UBSan) — catches UB
# Signed overflow, null dereference, misaligned access, shift errors
cmake -B build-ubsan \
    -DCMAKE_CXX_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=undefined"

# ThreadSanitizer (TSan) — catches data races
# Data races, lock-order inversions, deadlocks
cmake -B build-tsan \
    -DCMAKE_CXX_FLAGS="-fsanitize=thread -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"

# MemorySanitizer (MSan) — catches uninitialized reads (Clang only)
cmake -B build-msan \
    -DCMAKE_CXX_FLAGS="-fsanitize=memory -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=memory"

# Combine ASan + UBSan (commonly done together)
cmake -B build-asan-ubsan \
    -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"

# IMPORTANT: ASan and TSan cannot be combined — use separate builds
# IMPORTANT: Always compile ALL code with sanitizer flags (including libraries)

静态分析与代码覆盖率

静态分析在不运行代码的情况下检查你的代码,捕获 bug、风格违规和安全问题:

clang-tidy ── 最强大的 C++ 代码检查工具。检查现代化机会、性能问题、可读性和容易出 bug 的模式:

clang-tidy src/*.cpp -- -std=c++20 -I include
# Or with CMake:
cmake -B build -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=*,-fuchsia-*"

cppcheck ── 与 clang-tidy 互补,捕获不同类别的 bug:

cppcheck --enable=all --std=c++20 src/

代码覆盖率 ── 测量你的测试实际执行了哪些代码行:

cmake -B build-cov -DCMAKE_CXX_FLAGS="--coverage -g -O0"
cmake --build build-cov && cd build-cov && ctest
gcovr --html-details coverage.html -r ../src

对关键代码路径争取 80%+ 行覆盖率。100% 覆盖率很少值得付出努力 ── 专注于测试逻辑、边界情况和错误处理。

最佳实践

构建全面的测试策略:

1. 与代码同时编写测试 ── 而非之后。测试驱动开发(TDD)非常适合 C++:编写一个失败的测试、实现通过所需的最少代码、重构。
2. 在 CI 中运行消毒器 ── 为 ASan+UBSan 和 TSan 设置单独的 CI 任务。它们能捕获仅靠测试无法发现的 bug。
3. 使用测试夹具 进行共享资源的设置/清理(数据库连接、文件句柄、复杂对象)。
4. 测试边界情况和错误路径 ── 空输入、空容器、整数溢出、分配失败。
5. 保持测试快速 ── 单元测试应在几秒内运行。将慢速测试(网络、磁盘)移到集成测试套件中。
6. 优化前先做基准测试 ── 先测量,再优化。不要猜测瓶颈在哪里。

常见陷阱

测试实现而非行为:依赖内部数据结构的测试在重构时会中断。测试公共 API 和可观察行为,而非私有方法。

忽略消毒器报告:消毒器报告不是误报(极少数例外)。每个 ASan 或 UBSan 报告都指示真实的未定义行为,它将在某些平台或优化级别上导致问题。

不在优化模式下测试:某些 bug 仅在 -O2 或 -O3 下才会出现(尤其是优化器利用的未定义行为)。用 Debug 和 Release 构建都运行你的测试套件。

不稳定的测试:有时通过有时失败的测试比没有测试更糟 ── 它们会侵蚀对测试套件的信任。立即修复或删除不稳定的测试。

关键要点
  • GoogleTest 提供 EXPECT_*(非致命)和 ASSERT_*(致命)宏、测试夹具和参数化测试
  • 在 CMake 中使用 gtest_discover_tests() 自动向 CTest 注册测试
  • ASan 捕获内存错误,UBSan 捕获未定义行为,TSan 捕获数据竞争 ── 在 CI 中全部运行
  • 静态分析(clang-tidy、cppcheck)在不运行代码的情况下捕获 bug
  • 测试行为而非实现 ── 并始终测试边界情况和错误路径
  • 用 Debug 和 Release 构建都运行测试,以捕获依赖优化的 bug

测验 — 检验你的知识

(15 经验值)

1. GoogleTest 中 `EXPECT_EQ` 和 `ASSERT_EQ` 有什么区别?

2. 哪个消毒器检测释放后使用和缓冲区溢出 bug?

3. 为什么应该用 Debug 和 Release 构建都运行测试?