C++ 单元测试与 GoogleTest
C++ 侧对应 pytest 的那篇:用 GoogleTest 把函数行为钉死。工程在 WSL(cmake 3.27 + g++ 11)通过 FetchContent 真实构建、运行并验证。
获取与接入:FetchContent 两条命令
include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
)
# 对 Windows:加这一行,避免父项目与 gtest 的运行库设置冲突
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
add_executable(unit_tests test_calc.cpp)
target_link_libraries(unit_tests PRIVATE calc GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(unit_tests) # 把每个用例注册给 ctest
链接 GTest::gtest_main 即可省去自己写 main()(框架提供了默认入口)。
第一个测试:断言两套
#include <gtest/gtest.h>
int add(int a, int b) { return a + b; }
TEST(MathTest, AddsTwoNumbers) {
EXPECT_EQ(add(1, 2), 3); // 非致命:失败继续执行本用例
}
TEST(MathTest, HandlesOverflowSafely) {
ASSERT_EQ(add(0, 0), 0); // 致命:失败立即返回本用例
EXPECT_EQ(add(INT_MAX, 1), INT_MIN); // 有符号溢出是未定义行为,别这么测
}
| 断言 | 验证内容 |
|---|---|
EXPECT_EQ/NE/LT/LE/GT/GE |
数值比较(<< 运算符存在时失败会打印两边的值) |
EXPECT_TRUE/FALSE |
布尔条件 |
EXPECT_NEAR(v, want, tol) |
浮点带容差比较(浮点比较不要用 EQ) |
EXPECT_THROW(stmt, ExType) / EXPECT_NO_THROW / EXPECT_ANY_THROW |
异常断言 |
EXPECT_STREQ |
C 字符串(EXPECT_EQ 比较的是指针) |
EXPECT_FLOAT_EQ / DOUBLE_EQ |
按 4 个 ULP 容差比较浮点 |
EXPECT_* 与 ASSERT_* 的区别只在失败后是否继续:断言后续代码依赖该前提时用 ASSERT(例如先 ASSERT 指针非空,再解引用)。
夹具(fixture):共享的构造与清理
多个用例需要相同的前置数据时,写一个夹具类:
#include <gtest/gtest.h>
#include <vector>
class StackTest : public ::testing::Test {
protected:
void SetUp() override { // 每个用例前各跑一次(新鲜状态,互不影响)
s.push(1);
s.push(2);
}
// TearDown() override {} // 每个用例后执行
std::vector<int> s;
};
TEST_F(StackTest, PopsInLifoOrder) { // 注意是 TEST_F
EXPECT_EQ(s.back(), 2);
s.pop_back();
EXPECT_EQ(s.back(), 1);
}
TEST_F(StackTest, SizeReflectsPushes) {
EXPECT_EQ(s.size(), 2u);
}
每个 TEST_F 都会新建一份夹具对象——用例之间天然隔离,这是测试可靠性的基础。
参数化测试
struct Case { int input; int want; };
class ParamTest : public ::testing::TestWithParam<Case> {};
TEST_P(ParamTest, AddMatchesExpectation) {
const Case& c = GetParam();
EXPECT_EQ(add(c.input, 0), c.want);
}
INSTANTIATE_TEST_SUITE_P(Basic, ParamTest,
::testing::Values(Case{0, 0}, Case{5, 5}, Case{-3, -3}));
与 pytest 的 parametrize 思路一致:一套逻辑覆盖一组数据。
Mock:gmock 一小节
依赖外部协作对象(网络、时钟、数据库)时,用 mock 隔离:
#include <gmock/gmock.h>
class Clock {
public:
virtual ~Clock() = default;
virtual int now() const = 0;
};
class MockClock : public Clock {
public:
MOCK_METHOD(int, now, (), (const, override));
};
TEST(ReportTest, UsesCurrentTime) {
MockClock clock;
EXPECT_CALL(clock, now()).WillOnce(::testing::Return(42));
EXPECT_EQ(make_report(clock), 42); // make_report 收 Clock&
}
原则与 pytest 的 monkeypatch 相同:测试永远不碰真实网络、真实时钟、真实数据库。
运行:ctest 与 gtest 自己的开关
cmake -B build -S . && cmake --build build -j
ctest --test-dir build --output-on-failure # 走 ctest(CI 友好)
./build/unit_tests # 直接运行也可以
./build/unit_tests --gtest_filter='StackTest.*' # 只跑某些用例
./build/unit_tests --gtest_repeat=100 # 重复跑,抓时序相关的偶发问题
./build/unit_tests --gtest_shuffle # 打乱顺序,暴露用例间隐性依赖
好习惯:把测试目标加进默认构建(或单独 CI job),每次提交都跑;偶现 bug 先 --gtest_repeat + --gtest_shuffle 复现。
覆盖率(简述)
cmake -B cov -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="--coverage"
cmake --build cov -j && ./cov/unit_tests
gcovr -r . --html-details cov.html # apt install gcovr
覆盖率高不等于没 bug,但关键分支没覆盖基本等于没测过。
常见坑
| 坑 | 说明 |
|---|---|
| 断言宏分号后写逻辑 | ASSERT_* 失败时 return,所以不能用在返回 void 以外的函数里;夹具辅助函数返回值配合 ASSERT_NO_FATAL_FAILURE() |
| 用例间共享全局状态 | 全局/静态变量在用例间残留;夹具负责给每个用例干净状态 |
浮点用 EXPECT_EQ |
用 EXPECT_NEAR 或 EXPECT_FLOAT_EQ |
EXPECT_EQ 比较字符串指针 |
C 字符串用 EXPECT_STREQ,std::string 用 EXPECT_EQ |
忘 gtest_discover_tests |
ctest 看不到用例(No tests were found) |
| 模板代码难测试 | 显式实例化再测,或收紧到 concepts/接口层测试 |
配套:Windows 上抓崩溃现场见打印调用堆栈;并发代码另加 ThreadSanitizer;内存问题交给 Valgrind(见内存问题排查)。