Google Test(gtest)

从零开始,手把手带你掌握 C++ 最主流的单元测试框架。


目录

  1. 什么是 Google Test?为什么要用它?
  2. 环境搭建
  3. 第一个测试:五分钟上手
  4. 断言体系详解
  5. 测试夹具(Test Fixture)
  6. 参数化测试
  7. 类型参数化测试
  8. 死亡测试(Death Test)
  9. Mock 入门(Google Mock)
  10. 测试的运行与过滤
  11. 与 CMake 项目集成实战
  12. 最佳实践与常见踩坑
  13. 完整项目示例
  14. 附录:速查表

1. 什么是 Google Test?为什么要用它?

1.1 简介

Google Test(简称 gtest)是 Google 开源的 C++ 测试框架。它被广泛应用于工业级 C++ 项目中,包括 Chromium、LLVM、OpenCV 等知名项目。

1.2 核心优势

  • 跨平台:支持 Linux、macOS、Windows,兼容 GCC、Clang、MSVC 等主流编译器。
  • 零依赖:除标准库外不依赖其他第三方库。
  • 断言丰富:提供 EXPECT_*ASSERT_* 两大系列共几十种断言宏。
  • 自动发现测试:无需手动注册,写好宏即可被框架自动识别和执行。
  • 强大的过滤与输出:支持按名字过滤执行、生成 XML/JSON 报告、彩色终端输出。
  • 配套 Mock 框架:Google Mock 已合并进 gtest 仓库,可直接使用。

1.3 核心概念速览

在深入代码之前,先理解三个核心术语:

术语 含义 类比
Test(测试) 一个独立的测试函数 一道考试题
Test Suite(测试套件) 一组相关测试的集合 一份试卷
Test Program(测试程序) 包含所有测试套件的可执行文件 整场考试

它们的层级关系是:Test Program > Test Suite > Test


2. 环境搭建

Ubuntu / Debian:

sudo apt update
sudo apt install libgtest-dev cmake
# Ubuntu 20.04+ 通常自带编译好的库,如果没有则需要手动编译:
cd /usr/src/gtest
sudo cmake -B build
sudo cmake --build build
sudo cp build/lib/*.a /usr/lib/

macOS(Homebrew):

brew install googletest

Arch Linux:

sudo pacman -S gtest

验证安装

无论哪种方式,写一个最小测试来验证环境:

// verify.cpp
#include <gtest/gtest.h>

TEST(SanityCheck, TrueIsTrue) {
    EXPECT_TRUE(true);
}

编译并运行。如果看到绿色的 [ PASSED ] 1 test.,恭喜你,环境搭建成功。


3. 第一个测试:五分钟上手

3.1 被测代码

假设我们有一个简单的数学工具库:

// math_utils.h
#pragma once

int Add(int a, int b) {
    return a + b;
}

int Factorial(int n) {
    if (n < 0) return -1;   // 错误输入
    if (n == 0) return 1;
    return n * Factorial(n - 1);
}

3.2 编写测试

// test_math_utils.cpp
#include <gtest/gtest.h>
#include "math_utils.h"

// ---------- Add 函数的测试 ----------

// TEST 宏的两个参数:(测试套件名, 测试名)
TEST(AddTest, HandlesPositiveNumbers) {
    EXPECT_EQ(Add(1, 2), 3);
    EXPECT_EQ(Add(100, 200), 300);
}

TEST(AddTest, HandlesNegativeNumbers) {
    EXPECT_EQ(Add(-1, -2), -3);
    EXPECT_EQ(Add(-1, 1), 0);
}

TEST(AddTest, HandlesZero) {
    EXPECT_EQ(Add(0, 0), 0);
    EXPECT_EQ(Add(0, 5), 5);
}

// ---------- Factorial 函数的测试 ----------

TEST(FactorialTest, HandlesZero) {
    EXPECT_EQ(Factorial(0), 1);
}

TEST(FactorialTest, HandlesPositiveInput) {
    EXPECT_EQ(Factorial(1), 1);
    EXPECT_EQ(Factorial(5), 120);
    EXPECT_EQ(Factorial(10), 3628800);
}

TEST(FactorialTest, HandlesNegativeInput) {
    EXPECT_EQ(Factorial(-1), -1);
    EXPECT_EQ(Factorial(-100), -1);
}

3.3 运行结果解读

[==========] Running 6 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from AddTest
[ RUN      ] AddTest.HandlesPositiveNumbers
[       OK ] AddTest.HandlesPositiveNumbers (0 ms)
[ RUN      ] AddTest.HandlesNegativeNumbers
[       OK ] AddTest.HandlesNegativeNumbers (0 ms)
[ RUN      ] AddTest.HandlesZero
[       OK ] AddTest.HandlesZero (0 ms)
[----------] 3 tests from AddTest (0 ms total)

[----------] 3 tests from FactorialTest
[ RUN      ] FactorialTest.HandlesZero
[       OK ] FactorialTest.HandlesZero (0 ms)
[ RUN      ] FactorialTest.HandlesPositiveInput
[       OK ] FactorialTest.HandlesPositiveInput (0 ms)
[ RUN      ] FactorialTest.HandlesNegativeInput
[       OK ] FactorialTest.HandlesNegativeInput (0 ms)
[----------] 3 tests from FactorialTest (0 ms total)

[----------] Global test environment tear-down
[==========] 6 tests from 2 test suites ran. (0 ms total)
[  PASSED  ] 6 tests.

关键信息一目了然:执行了哪些套件、每个测试耗时多少、是否全部通过。


4. 断言体系详解

断言是 gtest 的灵魂。每个断言都有两种形式:

前缀 失败后行为 使用场景
EXPECT_* 记录失败,继续执行后续断言 绝大多数情况
ASSERT_* 记录失败,立即终止当前测试函数 后续断言依赖此结果时

经验法则:优先用 EXPECT_*,只在「如果这个失败了,后面的断言全没意义」时才用 ASSERT_*

4.1 布尔断言

EXPECT_TRUE(condition);     // condition 为 true 时通过
EXPECT_FALSE(condition);    // condition 为 false 时通过

4.2 数值比较

EXPECT_EQ(val1, val2);   // val1 == val2
EXPECT_NE(val1, val2);   // val1 != val2
EXPECT_LT(val1, val2);   // val1 <  val2
EXPECT_LE(val1, val2);   // val1 <= val2
EXPECT_GT(val1, val2);   // val1 >  val2
EXPECT_GE(val1, val2);   // val1 >= val2

4.3 字符串比较

EXPECT_STREQ(s1, s2);      // C 风格字符串相等(区分大小写)
EXPECT_STRNE(s1, s2);      // C 风格字符串不等
EXPECT_STRCASEEQ(s1, s2);  // 忽略大小写相等
EXPECT_STRCASENE(s1, s2);  // 忽略大小写不等

对于 std::string,直接使用 EXPECT_EQ 即可,因为它已重载了 ==

4.4 浮点数比较

浮点数不能直接用 == 比较,gtest 提供了专用断言:

EXPECT_FLOAT_EQ(val1, val2);   // float,容差约 4 ULP
EXPECT_DOUBLE_EQ(val1, val2);  // double,容差约 4 ULP
EXPECT_NEAR(val1, val2, abs_error);  // 自定义绝对误差

// 示例
EXPECT_FLOAT_EQ(1.0f / 3.0f, 0.333333f);      // 可能失败
EXPECT_NEAR(1.0 / 3.0, 0.333333, 1e-5);        // 通过

4.5 异常断言

// 期望抛出指定类型的异常
EXPECT_THROW(statement, exception_type);

// 期望抛出任意异常
EXPECT_ANY_THROW(statement);

// 期望不抛异常
EXPECT_NO_THROW(statement);

// 示例
EXPECT_THROW(vec.at(100), std::out_of_range);
EXPECT_NO_THROW(vec.at(0));

4.6 自定义失败信息

所有断言都支持用 << 追加自定义信息,失败时会一并输出:

EXPECT_EQ(result, expected)
    << "当输入为 n=" << n << " 时结果不符预期";

这在循环中调用断言时特别有用,能帮你快速定位是哪次迭代出了问题。

4.7 谓词断言(EXPECT_PRED)

当内置断言不够灵活时,可以使用自定义谓词:

// 一元谓词
bool IsEven(int n) { return n % 2 == 0; }
EXPECT_PRED1(IsEven, 4);     // 通过
EXPECT_PRED1(IsEven, 5);     // 失败,且会打印 "IsEven(5) evaluates to false"

// 二元谓词
bool IsCloseTo(double a, double b) { return std::abs(a - b) < 0.01; }
EXPECT_PRED2(IsCloseTo, 3.14, 3.141);

谓词断言的优势是:失败时会自动把函数名和参数值全部打印出来,调试非常方便。


5. 测试夹具(Test Fixture)

5.1 问题引入

假设多个测试都需要创建同一组对象、打开同一份文件、或者做同样的初始化工作。如果在每个 TEST 里都写一遍,代码重复且难以维护。

测试夹具(Fixture)就是用来解决这个问题的:把公共的初始化/清理逻辑抽到一个类里。

5.2 基本用法

#include <gtest/gtest.h>
#include <vector>

// 第一步:创建一个继承自 ::testing::Test 的类
class VectorTest : public ::testing::Test {
protected:
    // SetUp() 在每个测试执行之前调用
    void SetUp() override {
        vec.push_back(1);
        vec.push_back(2);
        vec.push_back(3);
    }

    // TearDown() 在每个测试执行之后调用
    void TearDown() override {
        // 这里做清理工作,比如关闭文件、释放资源
        // 对于 vector 来说不需要,析构函数会自动处理
    }

    // 成员变量,对每个测试用例可见
    std::vector<int> vec;
};

// 第二步:用 TEST_F 代替 TEST,第一个参数必须和夹具类名一致
TEST_F(VectorTest, InitialSizeIsThree) {
    EXPECT_EQ(vec.size(), 3);
}

TEST_F(VectorTest, CanPopBack) {
    vec.pop_back();
    EXPECT_EQ(vec.size(), 2);
    EXPECT_EQ(vec.back(), 2);
}

TEST_F(VectorTest, CanClear) {
    vec.clear();
    EXPECT_TRUE(vec.empty());
}

5.3 执行流程

每个 TEST_F 的完整执行流程如下:

1. 构造 VectorTest 对象(调用构造函数)
2. 调用 SetUp()
3. 执行测试体
4. 调用 TearDown()
5. 析构 VectorTest 对象(调用析构函数)

关键点:每个测试都会创建一个全新的夹具对象。所以 CanPopBack 里对 vec 的修改不会影响 CanClear,测试之间完全隔离。

5.4 SetUpTestSuite / TearDownTestSuite

如果某些初始化只需要在整个套件开始/结束时执行一次(比如连接数据库),可以使用静态方法:

class DatabaseTest : public ::testing::Test {
protected:
    static void SetUpTestSuite() {
        // 整个 DatabaseTest 套件开始前执行一次
        shared_conn = ConnectToDatabase();
    }

    static void TearDownTestSuite() {
        // 整个 DatabaseTest 套件结束后执行一次
        delete shared_conn;
        shared_conn = nullptr;
    }

    static DatabaseConnection* shared_conn;
};

DatabaseConnection* DatabaseTest::shared_conn = nullptr;

6. 参数化测试

6.1 问题引入

你写了一个判断素数的函数,想测试多组输入。如果逐个写 TEST,代码会很冗长:

TEST(PrimeTest, Input2)  { EXPECT_TRUE(IsPrime(2)); }
TEST(PrimeTest, Input3)  { EXPECT_TRUE(IsPrime(3)); }
TEST(PrimeTest, Input5)  { EXPECT_TRUE(IsPrime(5)); }
// ... 还有几十个

参数化测试可以把这种「同一个逻辑、不同输入数据」的场景变得极其简洁。

6.2 基本用法

#include <gtest/gtest.h>

bool IsPrime(int n) {
    if (n <= 1) return false;
    for (int i = 2; i * i <= n; ++i) {
        if (n % i == 0) return false;
    }
    return true;
}

// 第一步:继承 ::testing::TestWithParam<参数类型>
class PrimeTest : public ::testing::TestWithParam<int> {};

// 第二步:用 TEST_P 编写测试体,用 GetParam() 获取当前参数
TEST_P(PrimeTest, CheckPrime) {
    int n = GetParam();
    EXPECT_TRUE(IsPrime(n)) << n << " 应该是素数";
}

// 第三步:用 INSTANTIATE_TEST_SUITE_P 提供参数列表
INSTANTIATE_TEST_SUITE_P(
    SmallPrimes,               // 实例化前缀(用于区分同一套件的不同参数集)
    PrimeTest,                 // 测试套件名
    ::testing::Values(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)
);

INSTANTIATE_TEST_SUITE_P(
    LargePrimes,               // 实例化前缀(用于区分同一套件的不同参数集)
    PrimeTest,                 // 测试套件名
    ::testing::Values(10007, 10009, 10021, 10033, 10043, 10059, 10061, 10071, 10089, 10097)
);

运行时,gtest 会自动为每个参数值生成一个独立的测试:

SmallPrimes/PrimeTest.CheckPrime/0   (参数 = 2)
SmallPrimes/PrimeTest.CheckPrime/1   (参数 = 3)
SmallPrimes/PrimeTest.CheckPrime/2   (参数 = 5)
...

6.3 参数生成器

gtest 提供了多种参数生成器:

// 枚举具体值
::testing::Values(1, 2, 3, 4, 5)

// 区间 [start, end),步长 step
::testing::Range(0, 100, 10)    // 0, 10, 20, ..., 90

// 布尔值
::testing::Bool()               // true, false

// 笛卡尔积(组合参数)
::testing::Combine(
    ::testing::Values(1, 2),
    ::testing::Values("a", "b")
)   // (1,"a"), (1,"b"), (2,"a"), (2,"b")

// 从容器中获取
std::vector<int> data = {1, 2, 3};
::testing::ValuesIn(data)

6.4 多参数组合测试

对于多个参数的情况,使用 std::tuple + Combine

class MathTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};

TEST_P(MathTest, AdditionIsCorrect) {
    auto [a, b, expected] = GetParam();   // C++17 结构化绑定
    EXPECT_EQ(Add(a, b), expected);
}

INSTANTIATE_TEST_SUITE_P(
    AddCases,
    MathTest,
    ::testing::Values(
        std::make_tuple(1, 2, 3),
        std::make_tuple(-1, 1, 0),
        std::make_tuple(0, 0, 0),
        std::make_tuple(100, 200, 300)
    )
);

7. 类型参数化测试

当你写了一个模板类或模板函数,需要验证它对 intdoublefloat 等多种类型都能正确工作时,可以使用类型参数化测试。

7.1 TYPED_TEST(类型已知)

#include <gtest/gtest.h>

// 被测模板函数
template <typename T>
T MyAbs(T val) {
    return val < 0 ? -val : val;
}

// 第一步:定义类型列表
using MyTypes = ::testing::Types<int, float, double, long>;

// 第二步:夹具模板
template <typename T>
class AbsTest : public ::testing::Test {};

// 第三步:注册类型列表
TYPED_TEST_SUITE(AbsTest, MyTypes);

// 第四步:用 TYPED_TEST 编写测试,用 TypeParam 引用当前类型
TYPED_TEST(AbsTest, HandlesPositive) {
    TypeParam val = static_cast<TypeParam>(42);
    EXPECT_EQ(MyAbs(val), val);
}

TYPED_TEST(AbsTest, HandlesNegative) {
    TypeParam val = static_cast<TypeParam>(-42);
    EXPECT_EQ(MyAbs(val), static_cast<TypeParam>(42));
}

TYPED_TEST(AbsTest, HandlesZero) {
    TypeParam val = static_cast<TypeParam>(0);
    EXPECT_EQ(MyAbs(val), static_cast<TypeParam>(0));
}

运行时,每个 TYPED_TEST 都会对 intfloatdoublelong 各执行一次。


8. 死亡测试(Death Test)

8.1 什么是死亡测试

死亡测试用于验证程序在特定条件下是否会按预期终止(比如触发 assert、调用 abort()、或 exit())。这在测试错误处理逻辑时非常有用。

8.2 基本语法

void Crash() {
    int* p = nullptr;
    *p = 42;  // 解引用空指针,触发段错误
}

void ExitWithError() {
    std::cerr << "Fatal: something went wrong" << std::endl;
    exit(1);
}

TEST(DeathTest, CrashOnNullDeref) {
    // 期望 Crash() 导致进程异常终止
    EXPECT_DEATH(Crash(), "");
}

TEST(DeathTest, ExitWithMessage) {
    // 期望进程退出,且 stderr 输出匹配正则表达式
    EXPECT_EXIT(
        ExitWithError(),
        ::testing::ExitedWithCode(1),     // 退出码为 1
        "Fatal: something went wrong"     // stderr 匹配此正则
    );
}

8.3 常用死亡测试宏

// 期望语句导致进程死亡,stderr 匹配 regex
EXPECT_DEATH(statement, regex);

// 期望语句导致进程退出,且满足 predicate,stderr 匹配 regex
EXPECT_EXIT(statement, predicate, regex);

// 常用 predicate:
::testing::ExitedWithCode(exit_code)   // 进程以指定退出码退出
::testing::KilledBySignal(signal)      // 进程被指定信号杀死(仅 POSIX)

8.4 命名约定

gtest 建议死亡测试的套件名以 DeathTest 结尾,因为死亡测试的运行方式特殊(会 fork 子进程),gtest 默认会优先执行它们。


9. Mock 入门(Google Mock)

9.1 为什么需要 Mock

假设你的代码依赖一个数据库接口。你不可能在单元测试中真的去连数据库,你需要一个「替身」——这就是 Mock 对象。Mock 就是给依赖项做「替身」,用一个完全可控的假对象,替代真实的外部依赖,让单元测试能独立、快速、稳定地跑起来。

单元测试的核心原则是:只测自己的代码,不测别人的代码

但实际开发中,你的业务逻辑几乎一定会依赖外部模块,比如:

  • 数据库、Redis、MQ 等中间件
  • 第三方 API、RPC 服务
  • 硬件接口、文件系统
  • 其他团队 / 模块的代码

如果直接用真实依赖做单元测试,会遇到一堆问题:

  1. 环境依赖重:测试必须搭数据库、起服务,换环境就崩
  2. 执行慢:连数据库、发网络请求,单测从毫秒级变成秒级
  3. 不稳定:依赖服务挂了、网络波动,单测就失败,不是自己代码的锅
  4. 难构造场景:想测「数据库超时」「第三方服务返回 500」这种异常,真实环境很难复现

Mock 就是来解决这些问题的:用假对象模拟依赖的行为,完全绕开真实依赖。

9.2 基本使用流程

#include <gtest/gtest.h>
#include <gmock/gmock.h>

// 第一步:定义接口(抽象类)
class Database {
public:
    virtual ~Database() = default;
    virtual bool Connect(const std::string& server) = 0;
    virtual int GetRecord(int id) = 0;
    virtual bool Disconnect() = 0;
};

// 第二步:创建 Mock 类
class MockDatabase : public Database {
public:
    MOCK_METHOD(bool, Connect, (const std::string& server), (override));
    MOCK_METHOD(int,  GetRecord, (int id), (override));
    MOCK_METHOD(bool, Disconnect, (), (override));
};

// 第三步:被测代码(依赖 Database 接口)
class UserService {
public:
    explicit UserService(Database* db) : db_(db) {}

    std::string GetUserName(int id) {
        if (!db_->Connect("main-server")) {
            return "connection_failed";
        }
        int record = db_->GetRecord(id);
        db_->Disconnect();
        return "user_" + std::to_string(record);
    }

private:
    Database* db_;
};

// 第四步:编写测试
using ::testing::Return;
using ::testing::_;

TEST(UserServiceTest, ReturnsUserNameOnSuccess) {
    MockDatabase mock_db;

    // 设置期望:Connect 被调用一次,参数任意,返回 true
    EXPECT_CALL(mock_db, Connect(_))
        .Times(1)
        .WillOnce(Return(true));

    // 设置期望:GetRecord(42) 返回 1001
    EXPECT_CALL(mock_db, GetRecord(42))
        .Times(1)
        .WillOnce(Return(1001));

    // 设置期望:Disconnect 被调用一次
    EXPECT_CALL(mock_db, Disconnect())
        .Times(1)
        .WillOnce(Return(true));

    UserService service(&mock_db);
    EXPECT_EQ(service.GetUserName(42), "user_1001");
}

TEST(UserServiceTest, ReturnsErrorOnConnectionFailure) {
    MockDatabase mock_db;

    EXPECT_CALL(mock_db, Connect(_))
        .WillOnce(Return(false));

    // Connect 失败后不应调用 GetRecord 和 Disconnect
    EXPECT_CALL(mock_db, GetRecord(_)).Times(0);
    EXPECT_CALL(mock_db, Disconnect()).Times(0);

    UserService service(&mock_db);
    EXPECT_EQ(service.GetUserName(42), "connection_failed");
}

9.3 MOCK_METHOD 语法

MOCK_METHOD(返回类型, 方法名, (参数列表), (修饰符));

// 示例:
MOCK_METHOD(int, GetValue, (), (const, override));
MOCK_METHOD(void, SetValue, (int val), (override));
MOCK_METHOD(bool, Process, (const std::string& data, int count), (override));

9.4 常用匹配器

using namespace ::testing;

_                      // 匹配任意值
Eq(value)              // 等于 value
Ne(value)              // 不等于 value
Gt(value)              // 大于 value
Lt(value)              // 小于 value
HasSubstr("abc")       // 字符串包含 "abc"
StartsWith("pre")      // 字符串以 "pre" 开头
IsNull()               // 指针为 nullptr
NotNull()              // 指针非 nullptr

9.5 设置返回值

EXPECT_CALL(mock, Method(_))
    .WillOnce(Return(42))              // 第一次调用返回 42
    .WillOnce(Return(100))             // 第二次调用返回 100
    .WillRepeatedly(Return(-1));       // 后续所有调用返回 -1

10. 测试的运行与过滤

10.1 命令行参数

gtest 可执行文件支持丰富的命令行参数:

# 列出所有测试(不执行)
./my_tests --gtest_list_tests

# 只运行名字匹配通配符的测试
./my_tests --gtest_filter="AddTest.*"

# 运行多个套件
./my_tests --gtest_filter="AddTest.*:FactorialTest.*"

# 排除某些测试(减号前缀)
./my_tests --gtest_filter="*:-DeathTest.*"

# 重复运行 10 次(用于检测不稳定测试)
./my_tests --gtest_repeat=10

# 打乱测试执行顺序
./my_tests --gtest_shuffle

# 遇到第一个失败就停止
./my_tests --gtest_break_on_failure

# 输出 XML 报告(可用于 CI)
./my_tests --gtest_output="xml:report.xml"

# 输出 JSON 报告
./my_tests --gtest_output="json:report.json"

# 显示彩色输出
./my_tests --gtest_color=yes

# 打印每个测试的耗时
./my_tests --gtest_print_time=1

10.2 过滤表达式详解

--gtest_filter 的通配符规则:

符号 含义 示例
* 匹配任意字符序列 Add* 匹配所有以 Add 开头的
? 匹配任意单个字符 Test? 匹配 Test1、TestA 等
: 分隔多个模式(OR) A*:B* 匹配 A 开头或 B 开头
- 排除模式 *-Slow* 匹配所有,排除含 Slow 的

10.3 环境变量

所有命令行参数都有对应的环境变量形式:

export GTEST_FILTER="AddTest.*"
export GTEST_REPEAT=5
export GTEST_OUTPUT="xml:report.xml"
./my_tests   # 自动生效

11. 与 CMake 项目集成实战

11.1 推荐项目结构

my_project/
├── CMakeLists.txt          # 顶层 CMake
├── src/
│   ├── CMakeLists.txt
│   ├── calculator.h
│   └── calculator.cpp
└── tests/
    ├── CMakeLists.txt
    └── test_calculator.cpp

11.2 顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(my_project VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_subdirectory(src)
add_subdirectory(tests)

11.3 src/CMakeLists.txt

add_library(calculator calculator.cpp)
target_include_directories(calculator PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

11.4 src/calculator.h

#pragma once

class Calculator {
public:
    double Add(double a, double b);
    double Subtract(double a, double b);
    double Multiply(double a, double b);
    double Divide(double a, double b);  // 除零时抛 std::invalid_argument
};

11.5 src/calculator.cpp

#include "calculator.h"
#include <stdexcept>

double Calculator::Add(double a, double b) { return a + b; }
double Calculator::Subtract(double a, double b) { return a - b; }
double Calculator::Multiply(double a, double b) { return a * b; }

double Calculator::Divide(double a, double b) {
    if (b == 0.0) {
        throw std::invalid_argument("Division by zero");
    }
    return a / b;
}

11.6 tests/CMakeLists.txt

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.14.0
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(calculator_tests test_calculator.cpp)
target_link_libraries(calculator_tests
    PRIVATE
    calculator
    GTest::gtest_main
    GTest::gmock
)

include(GoogleTest)
gtest_discover_tests(calculator_tests)

11.7 tests/test_calculator.cpp

#include <gtest/gtest.h>
#include "calculator.h"

class CalculatorTest : public ::testing::Test {
protected:
    Calculator calc;
};

TEST_F(CalculatorTest, Add) {
    EXPECT_DOUBLE_EQ(calc.Add(1.5, 2.5), 4.0);
    EXPECT_DOUBLE_EQ(calc.Add(-1.0, 1.0), 0.0);
}

TEST_F(CalculatorTest, Subtract) {
    EXPECT_DOUBLE_EQ(calc.Subtract(10.0, 3.0), 7.0);
}

TEST_F(CalculatorTest, Multiply) {
    EXPECT_DOUBLE_EQ(calc.Multiply(3.0, 4.0), 12.0);
    EXPECT_DOUBLE_EQ(calc.Multiply(-2.0, 5.0), -10.0);
}

TEST_F(CalculatorTest, Divide) {
    EXPECT_DOUBLE_EQ(calc.Divide(10.0, 2.0), 5.0);
}

TEST_F(CalculatorTest, DivideByZeroThrows) {
    EXPECT_THROW(calc.Divide(1.0, 0.0), std::invalid_argument);
}

11.8 构建与运行

# 配置
cmake -B build -S .

# 编译
cmake --build build

# 运行测试(方式一:通过 ctest)
cd build && ctest --output-on-failure

# 运行测试(方式二:直接执行)
./build/tests/calculator_tests --gtest_color=yes

12. 最佳实践与常见踩坑

12.1 测试命名

好的命名让测试报告成为自带的文档。推荐格式:TEST(被测模块, 场景_期望行为)

// 好的命名
TEST(UserParser, EmptyInput_ReturnsNull)
TEST(UserParser, ValidJson_ReturnsUserObject)
TEST(UserParser, MissingNameField_ThrowsParseError)

// 差的命名
TEST(Test1, Test)
TEST(UserParser, Works)

12.2 一个测试只测一件事

// 不推荐:一个测试里塞了太多逻辑
TEST(Calculator, Everything) {
    EXPECT_EQ(calc.Add(1, 2), 3);
    EXPECT_EQ(calc.Multiply(3, 4), 12);
    EXPECT_THROW(calc.Divide(1, 0), std::invalid_argument);
}

// 推荐:拆分为独立测试
TEST(Calculator, Add_ReturnsSum) { ... }
TEST(Calculator, Multiply_ReturnsProduct) { ... }
TEST(Calculator, Divide_ByZero_Throws) { ... }

12.3 测试之间不要有依赖

gtest 不保证测试的执行顺序。每个测试都应该能独立运行。用 --gtest_shuffle 来检测你的测试是否存在隐藏的顺序依赖。

12.4 EXPECT vs ASSERT 的选择

TEST(ContainerTest, CheckContents) {
    std::vector<int> result = GetResult();

    // 这里用 ASSERT:如果 size 不对,后面的下标访问会越界
    ASSERT_EQ(result.size(), 3);

    // 这里用 EXPECT:即使某个元素错了,也想看到其他元素的结果
    EXPECT_EQ(result[0], 10);
    EXPECT_EQ(result[1], 20);
    EXPECT_EQ(result[2], 30);
}

12.5 常见踩坑

坑 1:TEST_F 的套件名写错了

class MyFixture : public ::testing::Test {};

// 编译错误!第一个参数必须与夹具类名完全一致
TEST_F(Myfixture, Something) { ... }  // 注意大小写

坑 2:忘记链接 gtest_main,然后没写 main() 函数

如果你没链接 GTest::gtest_main,需要自己提供 main()

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

坑 3:浮点数用 EXPECT_EQ

EXPECT_EQ(0.1 + 0.2, 0.3);  // 几乎一定失败!
EXPECT_NEAR(0.1 + 0.2, 0.3, 1e-10);  // 正确做法

坑 4:在 ASSERT 失败后仍有资源需要清理

ASSERT_* 失败后会直接 return,如果你手动分配了资源,可能会泄漏。解决方案是使用 RAII 或夹具的 TearDown()

坑 5:参数化测试忘记写 INSTANTIATE_TEST_SUITE_P

光写了 TEST_P 不写实例化宏,gtest 不会报错,但也不会执行任何测试。


13. 完整项目示例

项目结构

string_utils/
├── CMakeLists.txt
├── src/
│   ├── string_utils.h
│   └── string_utils.cpp
└── tests/
    ├── test_trim.cpp
    ├── test_split.cpp
    └── test_join.cpp

src/string_utils.h

#pragma once
#include <string>
#include <vector>

namespace strutil {

// 去除首尾空白
std::string Trim(const std::string& str);

// 按分隔符拆分字符串
std::vector<std::string> Split(const std::string& str, char delimiter);

// 用分隔符连接字符串
std::string Join(const std::vector<std::string>& parts, const std::string& sep);

} // namespace strutil

src/string_utils.cpp

#include "string_utils.h"
#include <algorithm>
#include <sstream>

namespace strutil {

std::string Trim(const std::string& str) {
    auto start = std::find_if_not(str.begin(), str.end(), ::isspace);
    auto end = std::find_if_not(str.rbegin(), str.rend(), ::isspace).base();
    return (start < end) ? std::string(start, end) : "";
}

std::vector<std::string> Split(const std::string& str, char delimiter) {
    std::vector<std::string> tokens;
    std::istringstream stream(str);
    std::string token;
    while (std::getline(stream, token, delimiter)) {
        tokens.push_back(token);
    }
    return tokens;
}

std::string Join(const std::vector<std::string>& parts, const std::string& sep) {
    std::string result;
    for (size_t i = 0; i < parts.size(); ++i) {
        if (i > 0) result += sep;
        result += parts[i];
    }
    return result;
}

} // namespace strutil

tests/test_trim.cpp

#include <gtest/gtest.h>
#include "string_utils.h"

TEST(TrimTest, RemovesLeadingSpaces) {
    EXPECT_EQ(strutil::Trim("   hello"), "hello");
}

TEST(TrimTest, RemovesTrailingSpaces) {
    EXPECT_EQ(strutil::Trim("hello   "), "hello");
}

TEST(TrimTest, RemovesBothSides) {
    EXPECT_EQ(strutil::Trim("  hello  "), "hello");
}

TEST(TrimTest, HandlesEmptyString) {
    EXPECT_EQ(strutil::Trim(""), "");
}

TEST(TrimTest, HandlesAllSpaces) {
    EXPECT_EQ(strutil::Trim("     "), "");
}

TEST(TrimTest, HandlesNoSpaces) {
    EXPECT_EQ(strutil::Trim("hello"), "hello");
}

TEST(TrimTest, HandlesTabs) {
    EXPECT_EQ(strutil::Trim("\thello\t"), "hello");
}

tests/test_split.cpp

#include <gtest/gtest.h>
#include "string_utils.h"

TEST(SplitTest, BasicSplit) {
    auto result = strutil::Split("a,b,c", ',');
    ASSERT_EQ(result.size(), 3);
    EXPECT_EQ(result[0], "a");
    EXPECT_EQ(result[1], "b");
    EXPECT_EQ(result[2], "c");
}

TEST(SplitTest, NoDelimiter) {
    auto result = strutil::Split("hello", ',');
    ASSERT_EQ(result.size(), 1);
    EXPECT_EQ(result[0], "hello");
}

TEST(SplitTest, EmptyString) {
    auto result = strutil::Split("", ',');
    ASSERT_EQ(result.size(), 1);
    EXPECT_EQ(result[0], "");
}

// 参数化测试:验证不同分隔符
class SplitDelimiterTest
    : public ::testing::TestWithParam<std::tuple<std::string, char, size_t>> {};

TEST_P(SplitDelimiterTest, ProducesCorrectTokenCount) {
    auto [input, delim, expected_count] = GetParam();
    auto result = strutil::Split(input, delim);
    EXPECT_EQ(result.size(), expected_count);
}

INSTANTIATE_TEST_SUITE_P(
    Various,
    SplitDelimiterTest,
    ::testing::Values(
        std::make_tuple("a.b.c", '.', 3),
        std::make_tuple("one::two::three", ':', 5),
        std::make_tuple("no-delim", ',', 1),
        std::make_tuple("a/b/c/d", '/', 4)
    )
);

tests/test_join.cpp

#include <gtest/gtest.h>
#include "string_utils.h"

TEST(JoinTest, BasicJoin) {
    EXPECT_EQ(strutil::Join({"a", "b", "c"}, ", "), "a, b, c");
}

TEST(JoinTest, SingleElement) {
    EXPECT_EQ(strutil::Join({"hello"}, ", "), "hello");
}

TEST(JoinTest, EmptyVector) {
    EXPECT_EQ(strutil::Join({}, ", "), "");
}

TEST(JoinTest, EmptySeparator) {
    EXPECT_EQ(strutil::Join({"a", "b", "c"}, ""), "abc");
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(string_utils VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 库
add_library(strutil src/string_utils.cpp)
target_include_directories(strutil PUBLIC src)

# 测试
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.14.0
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(strutil_tests
    tests/test_trim.cpp
    tests/test_split.cpp
    tests/test_join.cpp
)
target_link_libraries(strutil_tests PRIVATE strutil GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(strutil_tests)

14. 附录:速查表

断言速查

断言 含义
EXPECT_TRUE(cond) 条件为真
EXPECT_FALSE(cond) 条件为假
EXPECT_EQ(a, b) a == b
EXPECT_NE(a, b) a != b
EXPECT_LT(a, b) a < b
EXPECT_LE(a, b) a <= b
EXPECT_GT(a, b) a > b
EXPECT_GE(a, b) a >= b
EXPECT_FLOAT_EQ(a, b) float 近似相等
EXPECT_DOUBLE_EQ(a, b) double 近似相等
EXPECT_NEAR(a, b, err) |a - b| < err
EXPECT_STREQ(a, b) C 字符串相等
EXPECT_THROW(stmt, type) 抛出指定异常
EXPECT_ANY_THROW(stmt) 抛出任意异常
EXPECT_NO_THROW(stmt) 不抛异常
EXPECT_DEATH(stmt, regex) 进程终止

所有 EXPECT_* 均有对应的 ASSERT_* 版本。

宏速查

用途
TEST(Suite, Name) 定义一个普通测试
TEST_F(Fixture, Name) 定义使用夹具的测试
TEST_P(Suite, Name) 定义参数化测试
TYPED_TEST(Suite, Name) 定义类型参数化测试
INSTANTIATE_TEST_SUITE_P(...) 实例化参数化测试
MOCK_METHOD(ret, name, args, specs) 声明 Mock 方法
EXPECT_CALL(obj, method(matchers)) 设置 Mock 期望

命令行速查

参数 作用
--gtest_list_tests 列出所有测试
--gtest_filter=PATTERN 过滤测试
--gtest_repeat=N 重复运行 N 次
--gtest_shuffle 打乱顺序
--gtest_output=xml:FILE 输出 XML 报告
--gtest_break_on_failure 失败时中断
--gtest_color=yes 彩色输出
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐