测试、基准测试与消毒器
使用 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_* 用于致命断言)、用于共享设置/清理的测试夹具,以及参数化测试:
#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 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:
# 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 构建都运行测试?