跳转至

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(见内存问题排查)。

评论