Gtest
Google Test(gtest)
从零开始,手把手带你掌握 C++ 最主流的单元测试框架。
目录
- 什么是 Google Test?为什么要用它?
- 环境搭建
- 第一个测试:五分钟上手
- 断言体系详解
- 测试夹具(Test Fixture)
- 参数化测试
- 类型参数化测试
- 死亡测试(Death Test)
- Mock 入门(Google Mock)
- 测试的运行与过滤
- 与 CMake 项目集成实战
- 最佳实践与常见踩坑
- 完整项目示例
- 附录:速查表
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. 类型参数化测试
当你写了一个模板类或模板函数,需要验证它对 int、double、float 等多种类型都能正确工作时,可以使用类型参数化测试。
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 都会对 int、float、double、long 各执行一次。
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 服务
- 硬件接口、文件系统
- 其他团队 / 模块的代码
如果直接用真实依赖做单元测试,会遇到一堆问题:
- 环境依赖重:测试必须搭数据库、起服务,换环境就崩
- 执行慢:连数据库、发网络请求,单测从毫秒级变成秒级
- 不稳定:依赖服务挂了、网络波动,单测就失败,不是自己代码的锅
- 难构造场景:想测「数据库超时」「第三方服务返回 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 |
彩色输出 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)