Google Test(gtest/gmock)开源 C++ 单元测试与 Mock 框架完全指南

一、为什么要写单元测试,为什么选 Google Test

1.1 单元测试是什么(给小白的一句话类比)

单元测试 就像给一台机器逐颗螺丝做质检 :不等到整台机器装完才发现问题,而是每装一个零件就当场验证它转得对不对。在 C++ 里,"一颗螺丝"通常是一个函数、一个类或者一个模块,单元测试就是写一段小程序,调用被测代码并检查结果是否符合预期

  • 没有单元测试的代码:像写一封没有校对的长信,发出去(上线)才知道哪里有错别字。
  • 有单元测试的代码:像写完一句话就立刻检查一遍,错了当场改,成本极低。

1.2 为什么单元测试在 C++ 中尤其重要

C++ 是手工管理内存、没有垃圾回收的语言,常见问题(野指针、越界、资源泄漏、线程竞争)只有在特定运行路径下才会暴露。如果没有自动化测试,这些 Bug 往往要等上线后、由真实用户踩到才被发现------那时定位成本高出一个数量级。

1.3 Google Test 是什么

Google Test(简称 gtest ,配合的 Mock 框架叫 gmock )是 Google 开源的 C++ 单元测试框架,采用 Apache 2.0 许可证,可以放心用在个人项目和商业项目。它自 2008 年开源以来,已成为 C++ 社区事实上的行业标准

  • Chromium、LLVM/Clang、OpenCV、gRPC、Abseil 等大量知名项目都用它做测试;
  • 与 CMake/CTest 集成度极高,ctest 一条命令跑全部测试;
  • 功能全面:断言、测试夹具(Fixture)、参数化、死亡测试、Mock、事件监听、并行执行,开箱即用。

一句话总结:gtest 是 C++ 测试领域的"普通话"------学会了它,读任何主流开源项目的测试代码都不再吃力。


二、Google Test 的核心优点

2.1 优点总览

优点 说明 通俗类比
生态最广 Chromium / LLVM / OpenCV 等大项目背书,资料多、坑少 买手机选销量最高的型号,配件和教程最多
断言丰富 100+ 断言宏,覆盖比较、字符串、浮点、异常、死亡 工具箱里每种螺丝刀都有对应型号
Fixture 复用 TEST_F 自动为每个测试创建干净环境,防止互相污染 每个测试员都拿到一套全新未拆封的零件
参数化测试 一份测试逻辑批量跑多组输入 一个质检流程套用 100 组不同的数据
死亡测试 专门验证"程序会不会按预期崩溃" 故意测试安全气囊该弹出时是否弹出
gmock 强大 对依赖对象做 Mock,验证调用次数与参数 用替身演员拍危险戏,并记录他做了几个动作
CTest 集成 CMake 原生支持,ctest 一条命令管理全部用例 工厂总控台一键调度所有质检流水线
跨平台 Windows / Linux / macOS 全支持,MSVC/GCC/Clang 全兼容 同一套检验标准,全球工厂通用
运行快、输出友好 失败时给出预期值 vs 实际值 + 源码位置 报错单直接标明"哪个车间哪颗螺丝不合格"

2.2 与同类框架的对比

维度 Google Test Catch2 doctest Boost.Test
许可证 Apache 2.0 BSL-1.0 MIT BSL-1.0
引入方式 编译为静态库 / 也可单头文件 单头文件(header-only) 单头文件 需链接 Boost 库
编译速度 中等(需编译 lib) 较慢(模板重) 最快 中等
断言风格 EXPECT_EQ(a, b) 宏 REQUIRE(a == b) 宏 CHECK(a == b) 宏 BOOST_CHECK(a == b) 宏
测试命名 TEST(Suite, Name) TEST_CASE("...") TEST_CASE("...") BOOST_AUTO_TEST_CASE
Fixture TEST_F + SetUp/TearDown TEST_CASE_METHOD / Section TEST_FIXTURE BOOST_FIXTURE_TEST_CASE
参数化测试 INSTANTIATE_TEST_SUITE_P(丰富) GENERATE(较新) 支持 支持
死亡测试 极强(专门设计) 有限 有限 有限
Mock 框架 内置 gmock 需第三方(trompeloeil) 需第三方 需第三方
大型项目适配 最佳(Chromium/LLVM 验证)
输出报告 丰富(XML/JSON/人类可读) 丰富 简洁 丰富

结论:追求生态、死亡测试、Mock 一体化 → 选 gtest;追求极致轻量、快速编译、只写少量测试 → 可选 Catch2 或 doctest。


三、适用场景与同类框架对比

3.1 典型适用场景

场景 具体例子 用 gtest 解决什么
库/模块单元测试 字符串工具类、数学计算模块 每个函数独立验证,保证重构不破坏行为
算法正确性验证 排序、查找、图算法 用参数化测试一次跑几十组输入用例
网络/数据库/文件等外部依赖隔离 业务代码调用 HTTP 客户端、数据库 用 gmock 伪造外部依赖,让测试不依赖真实网络/文件,快且稳定
内存与资源管理验证 RAII 类、对象池、连接池 用死亡测试验证非法访问/断言失败路径
遗留代码改造 给老代码补测试后再重构 用 gtest 建立"安全网",重构后跑测试确认行为不变
CI 持续集成 GitLab CI / GitHub Actions 与 CTest 集成,每次提交自动跑全部用例

3.2 什么时候不该用 gtest

  • 只想写 3~5 个快速验证脚本 → 用 assert + 一个 main 就够;
  • 对编译速度极度敏感、测试文件巨大 → doctest 更轻;
  • 需要纯 BDD(Given-When-Then)风格 → Catch2 的 Section 更顺手;
  • 测试本身就是产品交付物的一部分、需要极简二进制 → 考虑单头文件方案。

四、环境搭建:三步把 gtest 跑起来

本节以 CMake + FetchContent 为例(最省事、不污染系统),同时给出 vcpkg 与直接编译两种方式。

第 1 步:准备编译器与 CMake

  • Windows:安装 Visual Studio(含 MSVC)或 MinGW-w64;CMake ≥ 3.14(FetchContent 需要)。
  • Linux:sudo apt install g++ cmake 或 sudo apt install clang cmake。
  • 验证:在终端执行 cmake --version,能看到版本号即就绪。

第 2 步:项目结构

复制代码
my_project/
├── CMakeLists.txt          # 构建脚本
├── src/
│   └── calculator.h        # 被测代码(头文件 + 实现)
│   └── calculator.cpp
└── tests/
    └── test_calculator.cpp # 测试代码

第 3 步:写 CMakeLists.txt(自动下载并编译 gtest)

复制代码
cmake_minimum_required(VERSION 3.14)
project(my_project CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 使用 FetchContent 自动拉取 gtest 源码(首次构建会联网下载)
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.14.0        # 建议锁定版本,保证可复现
)
FetchContent_MakeAvailable(googletest)

# 被测代码编译成静态库
add_library(calculator src/calculator.cpp)
target_include_directories(calculator PUBLIC src)

# 测试可执行文件
enable_testing()                        # 开启 CTest 支持
add_executable(test_calculator tests/test_calculator.cpp)
target_link_libraries(test_calculator PRIVATE calculator gtest_main)

# 注册到 CTest:ctest 命令就能发现并运行它
add_test(NAME test_calculator COMMAND test_calculator)

术语类比 :FetchContent 就像网购------告诉 CMake "我要 gtest 1.14.0",它自动下单(下载)、收货(解压)、安装(编译)。gtest_main 是 gtest 自带的 main 函数库,链接后你不用自己写 main

第 4 步:编译并运行

bash 复制代码
# Linux / macOS
mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure

# Windows(PowerShell,使用 VS 生成器)
mkdir build; cd build
cmake .. -A x64
cmake --build . --config Debug
ctest -C Debug --output-on-failure

看到类似输出即成功:

复制代码
[==========] Running 0 tests from 0 test suites.
[==========] 0 tests from 0 test suites ran. (0 ms total)
[  PASSED  ] 0 tests.

其他安装方式(任选其一)

  • vcpkg:vcpkg install gtest,然后在 CMake 中 find_package(GTest CONFIG REQUIRED),链接 GTest::gtest_main。
  • 手动编译:cmake -S googletest -B build && cmake --build build,把产物 libgtest.a / libgtest_main.a 放进自己的项目。

五、第一个测试:断言(EXPECT_ 与 ASSERT_)

5.1 被测代码:一个简单计算器

cpp 复制代码
// src/calculator.h
#pragma once

class Calculator {
public:
    // 加法:把两个整数相加
    int add(int a, int b) { return a + b; }

    // 除法:除数为 0 时返回 false,结果写入 result
    bool divide(int a, int b, int* result) {
        if (b == 0) return false;          // 除数不能为 0
        *result = a / b;
        return true;
    }
};

5.2 测试代码:TEST 宏 + 断言

cpp 复制代码
// tests/test_calculator.cpp
#include <gtest/gtest.h>      // 引入 gtest 头文件
#include "calculator.h"       // 引入被测代码

// TEST(测试套件名, 测试用例名):套件名用于分组,用例名描述具体行为
TEST(CalculatorTest, AddBasic) {
    Calculator calc;                          // 准备被测对象
    EXPECT_EQ(calc.add(1, 2), 3);             // 期望 1+2 == 3
    EXPECT_EQ(calc.add(-1, 1), 0);            // 负数相加
    EXPECT_EQ(calc.add(100, 200), 300);
}

TEST(CalculatorTest, AddLargeNumbers) {
    Calculator calc;
    // 数值过大时 int 会溢出,这是"预期行为",用 EXPECT 记录而不中断
    EXPECT_EQ(calc.add(2147483647, 1), -2147483648);   // 故意测试溢出
}

TEST(CalculatorTest, DivideByZeroReturnsFalse) {
    Calculator calc;
    int result = -1;
    // 除数为 0 时应返回 false,且不应修改 result
    EXPECT_FALSE(calc.divide(5, 0, &result));
    EXPECT_EQ(result, -1);
}

TEST(CalculatorTest, DivideNormal) {
    Calculator calc;
    int result = 0;
    EXPECT_TRUE(calc.divide(10, 2, &result)); // 正常除法应返回 true
    EXPECT_EQ(result, 5);
}

5.3 EXPECT_ 与 ASSERT_ 的区别(关键!)

宏前缀 失败后行为 使用建议
EXPECT_* 继续执行当前测试,记录失败 默认首选:能一次看到多个问题
ASSERT_* 立即终止当前测试 后续代码依赖本次结果时必须用(例如空指针检查后立即解引用)
cpp 复制代码
TEST(PointerTest, MustNotNull) {
    int* p = nullptr;
    // 如果 p 为空还继续往下走,解引用会直接崩溃,
    // 所以这里必须用 ASSERT 让测试"及时止损"
    ASSERT_NE(p, nullptr) << "p 不应为空";       // 失败立即返回
    EXPECT_EQ(*p, 42);                            // 只有 p 非空才会执行
}

<< "提示信息":失败时输出自定义提示,方便排查------相当于在错误单上写备注。

5.4 常用断言速查表

断言 含义 失败输出示例
EXPECT_EQ(a, b) / ASSERT_EQ(a, b) a == b Expected: 3 Actual: 4
EXPECT_NE(a, b) a != b ---
EXPECT_LT / LE / GT / GE < / <= / > / >= ---
EXPECT_TRUE(cond) 条件为真 ---
EXPECT_FALSE(cond) 条件为假 ---
EXPECT_STREQ(s1, s2) C 风格字符串相等(注意:不是 EXPECT_EQ) ---
EXPECT_STRNE(s1, s2) C 风格字符串不等 ---
EXPECT_FLOAT_EQ(a, b) 浮点数近似相等(4 位精度) ---
EXPECT_DOUBLE_EQ(a, b) double 近似相等 ---
EXPECT_NEAR(a, b, eps) 绝对误差不超过 eps ---
EXPECT_THROW(expr, Type) 表达式抛出 Type 异常 ---
EXPECT_NO_THROW(expr) 表达式不抛异常 ---
EXPECT_ANY_THROW(expr) 表达式抛出任意异常 ---

⚠️ 易错点 1 :比较 C 风格字符串 char* 必须用 EXPECT_STREQ,不能用 EXPECT_EQ------后者比较的是指针地址而不是字符串内容。两个内容相同的字符串,指针地址几乎必然不同。
⚠️ 易错点 2:浮点数直接 EXPECT_EQ(0.1 + 0.2, 0.3) 大概率失败(二进制浮点误差)。要用 EXPECT_DOUBLE_EQ 或 EXPECT_NEAR(x, y, 1e-9)。


六、Test Fixture:为多个测试准备共用环境

6.1 为什么需要 Fixture(类比)

假如你要测 5 个不同的功能,每个都需要"登录一个用户、打开数据库连接、初始化配置"。如果每个测试都重复写这段初始化代码,一旦初始化逻辑变化就要改 5 处------Fixture(夹具) 就是把这些重复准备动作集中到一处:每个测试开始前自动执行 SetUp(),结束后自动执行 TearDown(),并且每个测试都拿到全新的、互不干扰的环境

6.2 写法:继承 ::testing::Test

cpp 复制代码
// tests/test_vector_fixture.cpp
#include <gtest/gtest.h>
#include <vector>

// 定义一个 Fixture 类:继承 testing::Test
class VectorFixture : public ::testing::Test {
protected:
    // SetUp:每个测试用例运行前自动调用(类比:每次开工前先摆好工具)
    void SetUp() override {
        data = {1, 2, 3, 4, 5};          // 每个测试都从同样 5 个元素开始
    }

    // TearDown:每个测试用例运行后自动调用(类比:收工后清理现场)
    void TearDown() override {
        data.clear();
    }

    std::vector<int> data;               // 共享的被测数据(每个测试独立副本)
};

// 使用 TEST_F(夹具类名, 用例名),第一个参数必须是 Fixture 类名
TEST_F(VectorFixture, SizeIsFive) {
    EXPECT_EQ(data.size(), 5);           // 直接用成员变量,无需自己初始化
}

TEST_F(VectorFixture, PushBackIncreasesSize) {
    data.push_back(6);
    EXPECT_EQ(data.size(), 6);
    // 注意:即使上个测试修改了 data,这里仍然是 {1,2,3,4,5}
    // 因为每个测试执行前都会重新调用 SetUp()
}

6.3 执行顺序(重要)

复制代码
对于每个测试用例:
  1. 构造 VectorFixture 对象
  2. 调用 SetUp()          ← 初始化
  3. 执行 TEST_F 的函数体   ← 测试逻辑
  4. 调用 TearDown()       ← 清理
  5. 析构 VectorFixture 对象

✅ 每个测试之间完全隔离 :data 不会残留上一个测试的修改。这正是 Fixture 最大的价值------测试之间互相独立,跑 1 次和跑 100 次结果一致


七、参数化测试:一组数据跑一个测试

7.1 需求场景

测试"绝对值函数"时,你想测:正数、负数、零、最大负数......如果每个输入写一个 TEST,代码会非常啰嗦。参数化测试 (Parameterized Test)允许你只写一份测试逻辑,然后给它喂多组数据

7.2 三步写法

cpp 复制代码
// tests/test_parameterized.cpp
#include <gtest/gtest.h>

// 第 1 步:定义参数化测试类,继承 testing::TestWithParam<T>(T 是参数类型)
// 类比:先设计好"质检流程模板",参数就是"待质检的样品"
class AbsParamTest : public ::testing::TestWithParam<int> {
protected:
    // 被测函数:求绝对值
    int abs_value(int x) {
        return x < 0 ? -x : x;
    }
};

// 第 2 步:用 TEST_P 写测试逻辑,GetParam() 取出当前这组数据
TEST_P(AbsParamTest, WorksForAllInputs) {
    int input = GetParam();
    // 核心断言:绝对值一定 >= 0,且等于原数或其相反数
    EXPECT_GE(abs_value(input), 0);
    EXPECT_TRUE(abs_value(input) == input || abs_value(input) == -input);
}

// 第 3 步:用 INSTANTIATE_TEST_SUITE_P 提供数据集合
// 类比:把 6 份"样品"送进质检流程
INSTANTIATE_TEST_SUITE_P(
    AbsTestGroup,            // 实例化组名(任意,用于区分多组参数)
    AbsParamTest,            // 参数化测试类名
    ::testing::Values(0, 5, -5, 100, -100, 2147483647)  // 参数值列表
);

运行后你会看到 6 个独立测试:AbsTestGroup/AbsParamTest.WorksForAllInputs/0、/5、/-5......任何一个失败都不影响其他。

7.3 常用参数生成器

生成器 作用 示例
Values(a, b, c) 固定值列表 Values(1, 2, 3)
Range(begin, end, step) 整数/浮点范围 Range(0, 100, 10)
ValuesIn(容器) 来自 vector/数组 ValuesIn(my_vector)
Combine(g1, g2) 笛卡尔积(所有组合) Combine(Values(1,2), Values("a","b"))

⚠️ 易错点 3 :TEST_P 写测试逻辑,INSTANTIATE_TEST_SUITE_P 提供数据------两件事缺一不可。只写 TEST_P 不实例化,gtest 会报"没有实例化";只实例化没有 TEST_P 则没有任何测试运行。


八、类型参数化测试:一套代码测多种类型

如果被测逻辑对 int、double、float 都适用,可以为每种类型写一套测试吗?当然不必------类型参数化测试(Typed Test)让同一套测试代码在不同类型上各跑一遍。

cpp 复制代码
// tests/test_typed.cpp
#include <gtest/gtest.h>
#include <vector>

// 第 1 步:定义模板化的 Fixture,继承 testing::Test
template <typename T>
class ContainerTest : public ::testing::Test {
protected:
    std::vector<T> data;
};

// 第 2 步:声明要测的类型列表(类比:同一质检流程适配多种规格零件)
using MyTypes = ::testing::Types<int, double, float, long long>;
TYPED_TEST_SUITE(ContainerTest, MyTypes);

// 第 3 步:用 TYPED_TEST 写测试,TypeParam 就是当前类型
TYPED_TEST(ContainerTest, PushBackAndRead) {
    TypeParam value = static_cast<TypeParam>(42);   // 把 42 转成当前类型
    this->data.push_back(value);                     // 注意:必须用 this-> 访问成员
    EXPECT_EQ(this->data.size(), size_t(1));
    EXPECT_EQ(this->data[0], value);
}

TYPED_TEST(ContainerTest, EmptyInitially) {
    EXPECT_TRUE(this->data.empty());
}

⚠️ 易错点 4:在 TYPED_TEST 里访问 Fixture 成员必须写 this->data,不能只写 data。原因:模板类中的名字查找依赖两阶段查找(two-phase lookup),不加 this-> 编译器无法确认它是成员。这是 gtest 模板测试最常见的新手报错(编译不过)。


九、死亡测试:专门测试"崩溃"

9.1 概念(类比)

死亡测试用来验证"这段代码在特定条件下应当崩溃/终止 "------就像安全气囊测试:我们不是祈祷它不弹,而是验证它必须在撞击时弹出。在 C++ 里常用于测试:

  • assert() 断言失败路径;
  • 故意对 nullptr 解引用的防御性代码;
  • 非法参数导致进程终止的库接口。

9.2 写法

cpp 复制代码
// tests/test_death.cpp
#include <gtest/gtest.h>
#include <cassert>

int divide_guarded(int a, int b) {
    assert(b != 0 && "除数不能为 0");    // 断言失败会调用 abort() 终止进程
    return a / b;
}

// EXPECT_DEATH(语句, 正则):语句应当导致进程死亡,且死亡信息匹配正则
TEST(DeathTest, DivideByZeroAborts) {
    // "除数不能为 0" 是 assert 输出中包含的字符串(正则片段)
    EXPECT_DEATH(divide_guarded(1, 0), "除数不能为 0");
}

TEST(DeathTest, NormalCallDoesNotDie) {
    // EXPECT_NO_FATAL_FAILURE:验证正常路径不会死
    EXPECT_NO_FATAL_FAILURE(divide_guarded(8, 2));
}

⚠️ 易错点 5 :assert 在 Release 模式(NDEBUG)下不生效,死亡测试也会"不死亡"而失败。跑死亡测试请用 Debug 构建,或在 CI 中单独配置。
⚠️ 易错点 6 :死亡测试通过 fork 子进程执行(POSIX 平台),在多线程环境或依赖全局状态的代码中可能行为怪异;若被测代码本身启用了多线程,优先考虑用 EXPECT_EXIT + 显式退出码,或改用 gmock 验证错误处理逻辑。


十、gmock:用假对象隔离真实依赖

10.1 为什么需要 Mock(类比)

假设你测一个"订单服务",它内部依赖数据库短信网关 。真实跑测试意味着:连数据库、发真短信------慢、不稳定、还花钱。Mock(模拟对象) 就是找"替身演员":数据库替身返回预设数据,短信替身只记录"被调用了没、传了什么参数",从而让测试快、稳、不碰外部世界

10.2 三步使用 gmock

第 1 步:定义可 Mock 的接口(gmock 只能 mock 虚函数)

cpp 复制代码
// src/payment_gateway.h
#pragma once
#include <string>

// 支付网关接口:真实实现连第三方支付,测试时用 Mock 顶替
class PaymentGateway {
public:
    virtual ~PaymentGateway() = default;
    virtual bool charge(const std::string& user, double amount) = 0;
    virtual int   query_balance(const std::string& user) = 0;
};

第 2 步:用 MOCK_METHOD 生成 Mock 类

cpp 复制代码
// tests/mock_payment_gateway.h
#pragma once
#include <gmock/gmock.h>
#include "payment_gateway.h"

// 继承接口,用 MOCK_METHOD 声明"替身版本"的虚函数
class MockPaymentGateway : public PaymentGateway {
public:
    // MOCK_METHOD(返回类型, 函数名, (参数列表), (修饰符))
    MOCK_METHOD(bool, charge, (const std::string&, double), (override));
    MOCK_METHOD(int,  query_balance, (const std::string&), (override));
};

第 3 步:写业务代码 + 测试

cpp 复制代码
// src/order_service.h
#pragma once
#include "payment_gateway.h"

class OrderService {
public:
    explicit OrderService(PaymentGateway* gateway) : gateway_(gateway) {}

    // 下单:余额足够才扣款
    bool place_order(const std::string& user, double price) {
        if (gateway_->query_balance(user) < price) {
            return false;                        // 余额不足
        }
        return gateway_->charge(user, price);    // 扣款
    }

private:
    PaymentGateway* gateway_;
};
cpp 复制代码
// tests/test_order_service.cpp
#include <gmock/gmock.h>
#include "mock_payment_gateway.h"
#include "order_service.h"

using ::testing::Return;      // 指定 Mock 函数返回值
using ::testing::_;
using ::testing::Ge;          // 大于等于匹配器

TEST(OrderServiceTest, RejectsWhenBalanceNotEnough) {
    MockPaymentGateway mock;

    // 期望:query_balance("alice") 被调用,返回 50
    // 这里的 "alice" 是精确参数,50 是预设返回值
    EXPECT_CALL(mock, query_balance("alice"))
        .WillOnce(Return(50));

    OrderService service(&mock);
    // 订单金额 100 > 余额 50,应当拒绝下单
    EXPECT_FALSE(service.place_order("alice", 100.0));
    // 注意:charge 不应被调用(余额不足直接 return)
}

TEST(OrderServiceTest, ChargesWhenEnoughBalance) {
    MockPaymentGateway mock;

    // 期望:query_balance 被调用 1 次,返回 200
    EXPECT_CALL(mock, query_balance("bob"))
        .WillOnce(Return(200));

    // 期望:charge 被调用 1 次,参数为 ("bob", 100.0),返回 true
    EXPECT_CALL(mock, charge("bob", 100.0))
        .WillOnce(Return(true));

    OrderService service(&mock);
    EXPECT_TRUE(service.place_order("bob", 100.0));
}

10.3 常用 gmock 技巧速查

技巧 写法 说明
任意参数 EXPECT_CALL(mock, charge(_, _)) _ 匹配任意参数
匹配器 Ge(100), Lt(0), StrEq("abc") 参数范围/内容匹配
固定返回值 .WillOnce(Return(1)) 返回指定值
多次返回值 .WillOnce(Return(1)).WillOnce(Return(2)) 第一次返回 1,第二次返回 2
总是返回 .WillRepeatedly(Return(0)) 之后每次都返回 0
默认行为 ON_CALL(mock, query_balance(_)).WillByDefault(Return(0)) 未设置 EXPECT_CALL 时的兜底值
验证调用次数 .Times(2) / .Times(AtLeast(1)) 期望被调用次数
执行副作用 .WillOnce(Invoke(\[\](){ ... })) 调用真实逻辑/自定义 lambda

⚠️ 易错点 7 :EXPECT_CALL 不是"允许被调用",而是"必须 按设定被调用"------如果某条 EXPECT_CALL 没被触发,测试结束时 gmock 会报告 Uninteresting mock function call / Missing expected call 失败。反过来,如果只想要"允许调用但不管",请用 ON_CALL + WillByDefault。
⚠️ 易错点 8 :被测代码调用了 Mock 上没有 EXPECT_CALL 也没有 ON_CALL 的函数时,gmock 会打印警告并返回默认值(数值类型返回 0,bool 返回 false)。若确实不想管它,用 NiceMock<MockX> 消除警告;StrictMock<MockX> 则反过来:任何未期望调用都会直接判失败(更严格,适合关键接口)。


十一、与 CMake/CTest 集成:一键跑全部测试

11.1 单测可执行文件注册

在上一节 CMakeLists 基础上,为每个测试文件注册 add_test 即可(也支持一次注册多个):

复制代码
# 假设还有第二个测试可执行文件
add_executable(test_utils tests/test_utils.cpp)
target_link_libraries(test_utils PRIVATE gtest_main)

add_test(NAME test_utils COMMAND test_utils)

11.2 常用 CTest 命令

bash 复制代码
ctest                              # 跑所有注册的测试
ctest -R calculator                # 只跑名字匹配 calculator 的测试
ctest --output-on-failure          # 只显示失败用例的详细输出(CI 必配)
ctest -j 8                         # 8 个测试进程并行跑(多核加速)
ctest -N                           # 只列出有哪些测试,不执行

11.3 测试可执行文件自身的过滤参数(gtest 内置)

bash 复制代码
./test_calculator --gtest_filter=CalculatorTest.*     # 只跑 CalculatorTest 套件
./test_calculator --gtest_filter=*Add*                # 名字含 Add 的用例
./test_calculator --gtest_filter=-*Divide*            # 排除 Divide
./test_calculator --gtest_list_tests                  # 列出全部用例名
./test_calculator --gtest_repeat=3                    # 每个用例重复 3 次(查偶发失败)
./test_calculator --gtest_shuffle                     # 随机顺序运行(发现测试间依赖)
./test_calculator --gtest_break_on_failure            # 失败时中断到调试器

十二、高频易错点清单(⚠️ 必看)

易错点 现象 正确做法
1 EXPECT_EQ 比较 char* 字符串 内容相同却报失败 用 EXPECT_STREQ
2 直接比较浮点数 0.1+0.2 != 0.3 报失败 用 EXPECT_DOUBLE_EQ / EXPECT_NEAR
3 写了 TEST_P 忘写 INSTANTIATE_TEST_SUITE_P 编译通过但测试为 0 两者必须成对出现
4 TYPED_TEST 里不用 this-> 访问成员 编译报"找不到成员" 模板类内一律 this->data
5 Release 模式跑死亡测试 EXPECT_DEATH 不通过 Debug 构建再跑
6 多线程代码用死亡测试 结果不可预期 改用 EXPECT_EXIT 或 Mock
7 把 EXPECT_CALL 当"允许调用" 未触发时报 missing call 理解 EXPECT_CALL 是硬性期望;只想放行用 ON_CALL
8 Mock 未设置任何期望的函数被调用 打印警告 用 NiceMock 消除,或用 StrictMock 严格化
9 TEST_F 第一个参数不是 Fixture 类名 编译错误 必须是继承 ::testing::Test 的类名
10 在 SetUp 里做"昂贵"初始化但测试根本不用 每个用例都重复开销 改用一次性的 SetUpTestSuite / TearDownTestSuite(static)
11 测试之间共享全局状态 用例顺序影响结果 每个用例独立构造对象,避免全局可变状态
12 忘记 ASSERT_NE(p, nullptr) 就解引用 测试进程直接崩 空指针检查用 ASSERT_*(立即返回)

十三、运行技巧:过滤、重试与并行

13.1 定位偶发失败(Flaky Test)

偶发失败最常见原因是测试间数据竞争或顺序依赖。三步排查:

  1. --gtest_shuffle 打乱顺序跑几次,看是否随顺序变化;
  2. --gtest_repeat=100 --gtest_break_on_failure 重复 100 次并停在首次失败;
  3. 检查共享全局变量、未清理的静态数据、随机数种子。

13.2 并行加速

  • 同一进程内:gtest 的 TestWithParam 无法并行;推荐多进程并行------CTest 的 -j N 对多个测试可执行文件生效。
  • 若单个可执行文件测试很多,可拆成多个 add_test 条目(每个 COMMAND 加 --gtest_filter 分段),再 ctest -j。

13.3 输出 XML 报告(CI 需要)

复制代码
./test_calculator --gtest_output=xml:test_result.xml

生成的 XML 可被 Jenkins、GitLab CI 等直接解析展示趋势。


十四、FAQ 速查表

问题 答案
gtest 需要自己写 main 吗? 不需要,链接 gtest_main 即可;需要自定义初始化时链接 gtest 并自己写 main
gtest 支持 C++11 吗? 支持;gtest 1.14 要求 C++14 起,老项目用 1.10/1.11 支持 C++11
怎么只跑某个用例? --gtest_filter=SuiteName.CaseName
EXPECT 和 ASSERT 怎么选? 断言失败后还要继续跑就 EXPECT;失败后后续代码无法安全执行就 ASSERT
测试私有成员函数? 不推荐直接测私有;用 friend class + gtest 的 FRIEND_TEST,或把私有逻辑提取到匿名命名空间测试
怎么测试带文件 IO 的函数? 用 gmock Mock 文件接口,或让测试写入临时目录(std::filesystem::temp_directory_path())
gtest 与 Catch2 哪个好? 大型项目、死亡测试、Mock 一体化选 gtest;极致轻量与快速编译选 Catch2/doctest
Windows 上链接报一堆 LNK 错误? 检查是否 #define GTEST_LINKED_AS_SHARED_LIBRARY 1(使用 DLL 版 gtest 时);或统一用静态库版
测试代码会被编译进发布产物吗? 不会,只要 add_executable 只加在测试目标即可,不参与主程序链接
CI 里测试失败怎么快速定位? ctest --output-on-failure -R <名称> 只看失败的详细输出

十五、总结与延伸阅读

15.1 核心要点回顾

  1. 断言:EXPECT_* 失败继续、ASSERT_* 失败即停;字符串用 STREQ、浮点用 NEAR。
  2. Fixture:TEST_F + SetUp/TearDown 让每个测试环境干净隔离。
  3. 参数化:TEST_P + INSTANTIATE_TEST_SUITE_P 一份逻辑测多组数据;TYPED_TEST 一份逻辑测多种类型。
  4. 死亡测试:EXPECT_DEATH 验证"应当崩溃"的路径,注意 Debug 构建。
  5. gmock:MOCK_METHOD 生成替身,EXPECT_CALL 声明硬性期望,ON_CALL 提供默认行为。
  6. 集成:CMake FetchContent + add_test,ctest 一条命令全量回归。

15.2 延伸阅读

15.3 统一编译运行命令(本章全部示例)

复制代码
# 1) 项目根目录执行
mkdir -p build && cd build
cmake .. && cmake --build . -j

# 2) 运行全部测试(Windows Debug 构建加 -C Debug)
ctest --output-on-failure

# 3) 或直接运行测试可执行文件并查看详细输出
./test_calculator

提示:将示例代码放入 4.x 节的项目结构中,逐个添加 tests/test_*.cpp 并在 CMakeLists 中 add_executable + add_test,即可完整复现本章所有效果。

相关推荐
luj_176811 小时前
桥牌思维启示:系统设计的模块化架构
c语言·开发语言·c++·经验分享·算法
会周易的程序员11 小时前
aiDgePLC iec61131 虚拟机 完整使用文档
c++·物联网·架构·st·iec61131
冬奇Lab12 小时前
开源项目第190期:claude-video — 给 Claude 装上「眼睛」看视频,一条命令分析 YouTube/Loom/本地视频
人工智能·开源·claude
小小龙学IT12 小时前
Boost.Beast 深度实战:基于 Asio 的开源 C++ HTTP/WebSocket 协议库
c++·websocket·http
冬奇Lab12 小时前
Code Agent 解剖(04):系统提示词是怎么组装的,agent 的「人格」从哪来?
人工智能·开源·agent
不可求~13 小时前
C++ std::string_view 不是字符串:从悬空引用到安全用法
java·开发语言·c++
小小龙学IT14 小时前
C++ 正则表达式完全指南:从 std::regex 实战到 RE2 引擎原理(NFA/DFA/回溯陷阱)
c++·正则表达式
JackieZhengChina17 小时前
开源项目推荐:CodeSucker|本地离线开源软著源码鉴别材料生成工具
开源
码匠许师傅17 小时前
【C++ 面试真题】聊聊 C++ 的序列容器
java·c++·面试