开发少不了测试。但很多 C++ 开发者(包括曾经的我)对测试的态度是”先写代码再说,有空再补测试”——结果就是永远没空。
进入 AI 辅助编程时代后,测试变得更加重要而非更不重要。为什么?因为 AI 生成的代码你不一定完全理解,测试是你验证 AI 代码正确性的唯一武器。”测试先行”不再是理想主义,而是实用主义。
这篇文章用 Google Test(GTest)框架来讲——它是 C++ 生态中最主流的测试框架,Google 自家用,工业界广泛采用。
第一章:为什么 AI 时代更需要测试?
传统开发 vs AI 辅助开发
1
2
3
4
5
6
7
8
9
10
11
| 传统开发:
你写代码 → 你理解每一行 → 你"觉得"它对 → 上线 → 出 bug → 调试
AI 辅助开发:
你描述需求 → AI 生成代码 → 你看了一眼"好像对" → 上线 → 出 bug → ???
问题在哪?
├── AI 可能理解错你的需求
├── AI 生成的代码可能有边界情况的 bug
├── 你可能没完全理解 AI 写的代码
└── 代码"看起来对"不等于"真的对"
|
测试先行的正确姿势
1
2
3
4
5
6
7
8
9
10
| 正确的 AI 辅助开发流程:
1. 你先写测试(定义"什么是对的")
2. 让 AI 生成实现代码
3. 跑测试
├── 全绿 → 代码符合你的预期
└── 有红 → AI 的代码有问题,让它改或自己改
4. 你改需求时先改测试,再让 AI 改代码,再跑测试
核心思想:测试是"验收标准",先写标准再写答案
|
这和 TDD(Test-Driven Development)的理念完全一致:先写测试,再写实现,测试驱动开发。
第二章:Google Test 快速上手
安装和项目结构
最推荐的方式是用 CMake 的 FetchContent 直接拉取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| # CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(my_project)
set(CMAKE_CXX_STANDARD 17)
# 拉取 Google Test
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
)
FetchContent_MakeAvailable(googletest)
# 你的库代码
add_library(my_lib src/calculator.cpp)
target_include_directories(my_lib PUBLIC include)
# 测试可执行文件
enable_testing()
add_executable(my_tests
tests/test_calculator.cpp
)
target_link_libraries(my_tests
PRIVATE my_lib GTest::gtest_main
)
# 注册到 CTest
include(GoogleTest)
gtest_discover_tests(my_tests)
|
推荐的项目目录结构:
1
2
3
4
5
6
7
8
| my_project/
├── CMakeLists.txt
├── include/
│ └── calculator.h # 头文件
├── src/
│ └── calculator.cpp # 实现
└── tests/
└── test_calculator.cpp # 测试
|
你的第一个测试
先写头文件:
1
2
3
4
5
6
7
8
9
| // include/calculator.h
#pragma once
class Calculator {
public:
int add(int a, int b);
int subtract(int a, int b);
double divide(int a, int b);
};
|
再写测试(注意:TDD 要求先写测试再写实现,但为了讲解先把两者都给出来):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // tests/test_calculator.cpp
#include <gtest/gtest.h>
#include "calculator.h"
TEST(CalculatorTest, AddTwoPositiveNumbers) {
Calculator calc;
EXPECT_EQ(calc.add(1, 2), 3);
}
TEST(CalculatorTest, AddNegativeNumbers) {
Calculator calc;
EXPECT_EQ(calc.add(-1, -2), -3);
}
TEST(CalculatorTest, SubtractNumbers) {
Calculator calc;
EXPECT_EQ(calc.subtract(5, 3), 2);
}
TEST(CalculatorTest, DivideByZeroThrows) {
Calculator calc;
EXPECT_THROW(calc.divide(10, 0), std::invalid_argument);
}
|
最后写实现:
1
2
3
4
5
6
7
8
9
10
11
| // src/calculator.cpp
#include "calculator.h"
#include <stdexcept>
int Calculator::add(int a, int b) { return a + b; }
int Calculator::subtract(int a, int b) { return a - b; }
double Calculator::divide(int a, int b) {
if (b == 0) throw std::invalid_argument("Division by zero");
return static_cast<double>(a) / b;
}
|
构建和运行
1
2
3
4
5
6
7
8
9
| # 构建
mkdir build && cd build
cmake ..
cmake --build .
# 运行测试
ctest # CTest 方式
./my_tests # 直接运行
./my_tests --gtest_filter="CalculatorTest.*" # 只运行特定测试组
|
输出会像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
| [==========] Running 4 tests from 1 test suite.
[----------] 4 tests from CalculatorTest
[ RUN ] CalculatorTest.AddTwoPositiveNumbers
[ OK ] CalculatorTest.AddTwoPositiveNumbers (0 ms)
[ RUN ] CalculatorTest.AddNegativeNumbers
[ OK ] CalculatorTest.AddNegativeNumbers (0 ms)
[ RUN ] CalculatorTest.SubtractNumbers
[ OK ] CalculatorTest.SubtractNumbers (0 ms)
[ RUN ] CalculatorTest.DivideByZeroThrows
[ OK ] CalculatorTest.DivideByZeroThrows (0 ms)
[----------] 4 tests from CalculatorTest (0 ms total)
[==========] 4 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 4 tests.
|
第三章:断言体系 —— EXPECT vs ASSERT
Google Test 有两套断言:EXPECT_* 和 ASSERT_*。
区别一句话
1
2
3
4
5
6
| EXPECT_* :失败了继续跑后面的断言(非致命断言)
ASSERT_* :失败了立即终止当前测试(致命断言)
原则:
├── 默认用 EXPECT_*(一次跑出所有失败,方便定位)
└── 只在"后面的断言依赖这个结果"时用 ASSERT_*
|
常用断言速查表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| 布尔检查:
EXPECT_TRUE(condition) // 期望为 true
EXPECT_FALSE(condition) // 期望为 false
相等/不等:
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
字符串:
EXPECT_STREQ(str1, str2) // C 字符串相等
EXPECT_STRNE(str1, str2) // C 字符串不等
EXPECT_STRCASEEQ(str1, str2) // 忽略大小写相等
浮点数(不要用 EXPECT_EQ 比较浮点数!):
EXPECT_FLOAT_EQ(val1, val2) // float 近似相等(4 ULP 误差内)
EXPECT_DOUBLE_EQ(val1, val2) // double 近似相等
EXPECT_NEAR(val1, val2, abs_error) // 误差在 abs_error 内
异常:
EXPECT_THROW(statement, exception_type) // 抛出指定异常
EXPECT_ANY_THROW(statement) // 抛出任何异常
EXPECT_NO_THROW(statement) // 不抛异常
|
实际用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| TEST(StringTest, BasicOperations) {
std::string s = "Hello";
// 布尔
EXPECT_TRUE(s.size() > 0);
EXPECT_FALSE(s.empty());
// 相等
EXPECT_EQ(s.size(), 5);
EXPECT_EQ(s, "Hello"); // std::string 可以直接用 EXPECT_EQ
// 比较
EXPECT_GT(s.size(), 3);
}
TEST(FloatTest, PrecisionMatters) {
double result = 0.1 + 0.2;
// ❌ 错误:浮点数不要用 EXPECT_EQ
// EXPECT_EQ(result, 0.3); // 会失败!0.1+0.2 != 0.3 (浮点精度)
// ✅ 正确
EXPECT_NEAR(result, 0.3, 1e-10);
EXPECT_DOUBLE_EQ(0.5 + 0.5, 1.0); // 精确可表示的浮点数可以用
}
TEST(PointerTest, UseAssertForPointer) {
int* ptr = getPointer(); // 可能返回 nullptr
// 用 ASSERT:如果 ptr 是空的,后面解引用会崩溃
ASSERT_NE(ptr, nullptr);
// 到这里 ptr 一定不为空
EXPECT_EQ(*ptr, 42);
}
|
自定义失败信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| TEST(CustomMessageTest, WithContext) {
int value = computeSomething();
EXPECT_EQ(value, 42)
<< "computeSomething() 返回了 " << value
<< ",但期望是 42";
// 失败时会输出:
// Expected equality of these values:
// value
// Which is: 17
// 42
// computeSomething() 返回了 17,但期望是 42
}
|
第四章:Test Fixture —— 共享测试环境
多个测试用例需要相同的初始化环境时,用 Test Fixture。
为什么需要 Fixture?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // ❌ 不用 Fixture:每个测试都重复初始化
TEST(DatabaseTest, Insert) {
Database db;
db.connect("localhost", 3306);
db.login("admin", "password");
// ... 测试 insert
db.disconnect();
}
TEST(DatabaseTest, Query) {
Database db;
db.connect("localhost", 3306); // 重复!
db.login("admin", "password"); // 重复!
// ... 测试 query
db.disconnect();
}
// ✅ 用 Fixture:统一初始化和清理
|
Fixture 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| class DatabaseTest : public ::testing::Test {
protected:
Database db;
void SetUp() override {
// 每个 TEST_F 之前自动调用
db.connect("localhost", 3306);
db.login("admin", "password");
}
void TearDown() override {
// 每个 TEST_F 之后自动调用
db.disconnect();
}
};
// 注意:用 TEST_F 不是 TEST!
TEST_F(DatabaseTest, InsertRecord) {
// db 已经连接好了
EXPECT_TRUE(db.insert("users", "Alice"));
EXPECT_EQ(db.count("users"), 1);
}
TEST_F(DatabaseTest, QueryRecord) {
// 每个测试都有全新的 db(SetUp 重新执行)
db.insert("users", "Bob");
auto result = db.query("users", "Bob");
EXPECT_EQ(result.name, "Bob");
}
|
Fixture 的生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 每个 TEST_F 的执行流程:
构造 Fixture 对象(调用构造函数)
│
▼
SetUp()(初始化测试环境)
│
▼
执行测试体(你写的 TEST_F 代码)
│
▼
TearDown()(清理测试环境)
│
▼
析构 Fixture 对象(调用析构函数)
注意:每个 TEST_F 都创建新的 Fixture 实例!
测试之间完全隔离,互不影响。
|
构造/析构 vs SetUp/TearDown
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| class MyFixture : public ::testing::Test {
protected:
// 方式 1:构造函数/析构函数
MyFixture() {
// 初始化(不能用 ASSERT,因为构造函数没有返回值)
}
~MyFixture() override {
// 清理(不建议抛异常)
}
// 方式 2:SetUp/TearDown
void SetUp() override {
// 初始化(可以用 ASSERT!失败会跳过测试体)
}
void TearDown() override {
// 清理(可以检查测试是否失败:HasFailure())
}
};
// 推荐:
// - 简单的成员初始化 → 构造函数
// - 可能失败的初始化(需要 ASSERT)→ SetUp
// - 清理逻辑 → TearDown(可以根据测试结果做不同清理)
|
实际案例:测试一个 LRU Cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| #include <gtest/gtest.h>
#include "lru_cache.h"
class LRUCacheTest : public ::testing::Test {
protected:
LRUCache<std::string, int> cache{3}; // 容量为 3 的缓存
void SetUp() override {
// 预填充一些数据
cache.put("a", 1);
cache.put("b", 2);
cache.put("c", 3);
}
};
TEST_F(LRUCacheTest, GetExistingKey) {
auto val = cache.get("b");
ASSERT_TRUE(val.has_value());
EXPECT_EQ(val.value(), 2);
}
TEST_F(LRUCacheTest, GetNonExistingKey) {
auto val = cache.get("z");
EXPECT_FALSE(val.has_value());
}
TEST_F(LRUCacheTest, EvictsLRUWhenFull) {
// 缓存已满(a, b, c),再插入 d
cache.put("d", 4);
// a 是最久未使用的,应该被淘汰
EXPECT_FALSE(cache.get("a").has_value());
// b, c, d 还在
EXPECT_TRUE(cache.get("b").has_value());
EXPECT_TRUE(cache.get("c").has_value());
EXPECT_TRUE(cache.get("d").has_value());
}
TEST_F(LRUCacheTest, AccessUpdatesRecency) {
// 访问 a,使其变为最近使用
cache.get("a");
// 插入 d,应该淘汰 b(现在 b 是最久未使用的)
cache.put("d", 4);
EXPECT_TRUE(cache.get("a").has_value()); // a 刚被访问过
EXPECT_FALSE(cache.get("b").has_value()); // b 被淘汰
}
|
第五章:参数化测试 —— 一套逻辑多组数据
当你要对同一个函数用不同输入测试时,参数化测试避免大量重复代码。
基本参数化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <gtest/gtest.h>
#include "math_utils.h"
// 第 1 步:定义参数化测试类
class IsPrimeTest : public ::testing::TestWithParam<int> {};
// 第 2 步:写测试逻辑
TEST_P(IsPrimeTest, PrimeNumbers) {
int n = GetParam(); // 获取当前参数
EXPECT_TRUE(isPrime(n)) << n << " should be prime";
}
// 第 3 步:提供参数
INSTANTIATE_TEST_SUITE_P(
Primes, // 实例名
IsPrimeTest, // 测试类
::testing::Values(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)
);
// 等价于你手写了 10 个 TEST!
|
多参数 —— 用 struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| struct DivisionCase {
int dividend;
int divisor;
double expected;
// 让测试名字更可读
friend std::ostream& operator<<(std::ostream& os, const DivisionCase& c) {
return os << c.dividend << "/" << c.divisor << "=" << c.expected;
}
};
class DivisionTest : public ::testing::TestWithParam<DivisionCase> {};
TEST_P(DivisionTest, CorrectResult) {
auto [dividend, divisor, expected] = GetParam();
Calculator calc;
EXPECT_DOUBLE_EQ(calc.divide(dividend, divisor), expected);
}
INSTANTIATE_TEST_SUITE_P(
DivisionCases,
DivisionTest,
::testing::Values(
DivisionCase{10, 2, 5.0},
DivisionCase{7, 2, 3.5},
DivisionCase{0, 5, 0.0},
DivisionCase{-10, 2, -5.0},
DivisionCase{10, -2, -5.0}
)
);
|
参数生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Values:逐个指定
::testing::Values(1, 2, 3, 4, 5)
// Range:范围(start, end, step)
::testing::Range(0, 100, 10) // 0, 10, 20, ..., 90
// Bool:true 和 false
::testing::Bool() // true, false
// Combine:笛卡尔积(多维参数组合)
::testing::Combine(
::testing::Values("http", "https"),
::testing::Values(80, 443, 8080)
)
// 生成:("http",80), ("http",443), ("http",8080),
// ("https",80), ("https",443), ("https",8080)
// ValuesIn:从容器中取值
std::vector<int> data = loadTestData();
::testing::ValuesIn(data)
|
实际案例:测试字符串处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| struct TrimCase {
std::string input;
std::string expected;
};
class TrimTest : public ::testing::TestWithParam<TrimCase> {};
TEST_P(TrimTest, RemovesLeadingAndTrailingSpaces) {
auto [input, expected] = GetParam();
EXPECT_EQ(trim(input), expected);
}
INSTANTIATE_TEST_SUITE_P(
TrimCases,
TrimTest,
::testing::Values(
TrimCase{" hello ", "hello"},
TrimCase{"hello", "hello"}, // 无空格
TrimCase{" ", ""}, // 全空格
TrimCase{"", ""}, // 空串
TrimCase{"\t hello \n", "hello"}, // 制表符和换行
TrimCase{"hello world", "hello world"} // 中间的空格保留
)
);
|
第六章:Google Mock —— 模拟依赖
真实项目中,你要测试的代码往往依赖数据库、网络、文件系统等外部服务。Mock 让你模拟这些依赖,只测试你的逻辑。
为什么需要 Mock?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 不用 Mock:
你的代码 → 调用真实数据库 → 需要数据库环境
问题:
├── 测试慢(网络延迟)
├── 不可重复(数据库数据会变)
├── 难以测试异常路径(怎么让数据库出错?)
└── CI 环境可能没有数据库
用 Mock:
你的代码 → 调用 Mock 对象 → Mock 返回预设值
优点:
├── 测试快(内存中模拟)
├── 可重复(行为完全可控)
├── 易测异常(让 Mock 抛异常)
└── 无外部依赖
|
Mock 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // 第 1 步:定义接口(面向接口编程!)
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual bool connect(const std::string& host) = 0;
virtual std::string query(const std::string& sql) = 0;
virtual bool insert(const std::string& table, const std::string& data) = 0;
};
// 第 2 步:创建 Mock 类
#include <gmock/gmock.h>
class MockDatabase : public IDatabase {
public:
MOCK_METHOD(bool, connect, (const std::string& host), (override));
MOCK_METHOD(std::string, query, (const std::string& sql), (override));
MOCK_METHOD(bool, insert, (const std::string& table, const std::string& data), (override));
};
// 第 3 步:你的业务代码依赖接口
class UserService {
IDatabase& db;
public:
explicit UserService(IDatabase& database) : db(database) {}
bool createUser(const std::string& name) {
if (name.empty()) return false;
return db.insert("users", name);
}
std::string findUser(int id) {
return db.query("SELECT * FROM users WHERE id=" + std::to_string(id));
}
};
|
使用 Mock 写测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| using ::testing::Return;
using ::testing::_;
using ::testing::HasSubstr;
TEST(UserServiceTest, CreateUserSuccess) {
MockDatabase mockDb;
UserService service(mockDb);
// 设置期望:当调用 insert("users", "Alice") 时返回 true
EXPECT_CALL(mockDb, insert("users", "Alice"))
.Times(1)
.WillOnce(Return(true));
EXPECT_TRUE(service.createUser("Alice"));
}
TEST(UserServiceTest, CreateUserEmptyName) {
MockDatabase mockDb;
UserService service(mockDb);
// 空名字不应该调用数据库
EXPECT_CALL(mockDb, insert(_, _)).Times(0);
EXPECT_FALSE(service.createUser(""));
}
TEST(UserServiceTest, FindUserCallsCorrectSQL) {
MockDatabase mockDb;
UserService service(mockDb);
// 验证 SQL 包含正确的 ID
EXPECT_CALL(mockDb, query(HasSubstr("id=42")))
.WillOnce(Return("Alice"));
auto result = service.findUser(42);
EXPECT_EQ(result, "Alice");
}
TEST(UserServiceTest, CreateUserDatabaseFails) {
MockDatabase mockDb;
UserService service(mockDb);
// 模拟数据库故障
EXPECT_CALL(mockDb, insert(_, _))
.WillOnce(Return(false));
EXPECT_FALSE(service.createUser("Alice"));
}
|
EXPECT_CALL 详解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| EXPECT_CALL(mock_object, method_name(matchers))
.Times(cardinality) // 调用次数
.WillOnce(action) // 第一次调用时的行为
.WillRepeatedly(action); // 后续调用时的行为
// 次数(Times)
.Times(0) // 不应该被调用
.Times(1) // 恰好调用 1 次
.Times(3) // 恰好 3 次
.Times(::testing::AtLeast(2)) // 至少 2 次
.Times(::testing::AtMost(5)) // 最多 5 次
.Times(::testing::Between(2, 5)) // 2 到 5 次
// 动作(Actions)
.WillOnce(Return(42)) // 返回 42
.WillOnce(Throw(std::runtime_error("boom"))) // 抛异常
.WillOnce(DoDefault()) // 执行默认行为
.WillRepeatedly(Return(0)) // 后续每次都返回 0
|
匹配器(Matchers)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| using namespace ::testing;
// 通配符
EXPECT_CALL(mock, method(_)) // 任意值
EXPECT_CALL(mock, method(_, _)) // 任意两个参数
// 比较
EXPECT_CALL(mock, method(Eq(42))) // 等于 42
EXPECT_CALL(mock, method(Ne(0))) // 不等于 0
EXPECT_CALL(mock, method(Gt(10))) // 大于 10
EXPECT_CALL(mock, method(Le(100))) // 小于等于 100
// 字符串
EXPECT_CALL(mock, method(HasSubstr("hello"))) // 包含 "hello"
EXPECT_CALL(mock, method(StartsWith("http"))) // 以 "http" 开头
EXPECT_CALL(mock, method(MatchesRegex("\\d+"))) // 匹配正则
// 组合
EXPECT_CALL(mock, method(AllOf(Gt(0), Lt(100)))) // 0 < x < 100
EXPECT_CALL(mock, method(AnyOf(Eq(1), Eq(2)))) // 1 或 2
EXPECT_CALL(mock, method(Not(Eq(0)))) // 不是 0
// 容器
EXPECT_CALL(mock, method(ElementsAre(1, 2, 3))) // 恰好 [1,2,3]
EXPECT_CALL(mock, method(Contains(42))) // 包含 42
EXPECT_CALL(mock, method(UnorderedElementsAre(3, 1, 2))) // 无序 [1,2,3]
EXPECT_CALL(mock, method(IsEmpty())) // 空容器
|
第七章:TDD 工作流 —— 红绿重构
TDD(Test-Driven Development)的核心循环:
1
2
3
4
5
6
7
8
9
| ┌──────────────────────────────────────────┐
│ │
│ RED → GREEN → REFACTOR → RED → ... │
│ │
│ 红:写一个失败的测试 │
│ 绿:写最少的代码让测试通过 │
│ 重构:改善代码结构但不改变行为 │
│ │
└──────────────────────────────────────────┘
|
完整 TDD 示例:实现一个 Stack
第 1 轮:空栈
1
2
3
4
5
6
| // 先写测试(RED)
TEST(StackTest, NewStackIsEmpty) {
Stack<int> stack;
EXPECT_TRUE(stack.empty());
EXPECT_EQ(stack.size(), 0);
}
|
1
2
3
4
5
6
7
| // 再写最少代码让测试通过(GREEN)
template<typename T>
class Stack {
public:
bool empty() const { return true; }
size_t size() const { return 0; }
};
|
第 2 轮:Push 一个元素
1
2
3
4
5
6
7
| // 先写测试(RED)
TEST(StackTest, PushOneElement) {
Stack<int> stack;
stack.push(42);
EXPECT_FALSE(stack.empty());
EXPECT_EQ(stack.size(), 1);
}
|
1
2
3
4
5
6
7
8
9
| // 写实现让测试通过(GREEN)
template<typename T>
class Stack {
std::vector<T> data;
public:
bool empty() const { return data.empty(); }
size_t size() const { return data.size(); }
void push(const T& value) { data.push_back(value); }
};
|
第 3 轮:Pop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 先写测试(RED)
TEST(StackTest, PopReturnsLastPushed) {
Stack<int> stack;
stack.push(1);
stack.push(2);
EXPECT_EQ(stack.pop(), 2);
EXPECT_EQ(stack.pop(), 1);
EXPECT_TRUE(stack.empty());
}
TEST(StackTest, PopEmptyThrows) {
Stack<int> stack;
EXPECT_THROW(stack.pop(), std::out_of_range);
}
|
1
2
3
4
5
6
7
| // 写实现(GREEN)
T pop() {
if (data.empty()) throw std::out_of_range("Stack is empty");
T value = data.back();
data.pop_back();
return value;
}
|
第 4 轮:Top(REFACTOR 时发现需要)
1
2
3
4
5
6
| TEST(StackTest, TopDoesNotRemove) {
Stack<int> stack;
stack.push(42);
EXPECT_EQ(stack.top(), 42);
EXPECT_EQ(stack.size(), 1); // 还在
}
|
TDD 的关键原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 1. 一次只写一个失败的测试
不要一口气写 10 个测试再去实现——你会迷失方向
2. 写"最少的代码"让测试通过
不要提前优化,不要想着"将来可能需要"
3. 测试名字要描述行为,不是描述实现
✅ PopReturnsLastPushed
❌ TestPopMethodWithVector
4. 每个测试只测一个行为
一个 TEST 里不要验证 5 个不相关的事情
5. 先测 Happy Path,再测 Edge Case
├── 正常输入
├── 边界值(0、空、最大值)
├── 异常路径
└── 并发场景(如果适用)
|
第八章:测试组织和最佳实践
测试命名
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Google 推荐的命名格式:
// TEST(测试套件名, 测试场景_期望行为)
// ✅ 好的命名
TEST(UserParser, EmptyInput_ReturnsNullopt)
TEST(UserParser, ValidJson_ReturnsUser)
TEST(UserParser, MissingNameField_ThrowsParseError)
TEST(Cache, EvictsLRU_WhenCapacityExceeded)
// ❌ 不好的命名
TEST(Test1, Test)
TEST(UserParser, Works)
TEST(UserParser, TestParse)
|
测试文件组织
1
2
3
4
5
6
7
8
9
10
11
12
13
| tests/
├── test_calculator.cpp # 一个模块一个测试文件
├── test_user_service.cpp
├── test_cache.cpp
├── test_string_utils.cpp
├── mocks/ # Mock 类集中管理
│ ├── mock_database.h
│ └── mock_http_client.h
├── fixtures/ # 复杂 Fixture
│ └── database_fixture.h
└── test_data/ # 测试数据文件
├── valid_user.json
└── invalid_input.txt
|
AAA 模式
每个测试遵循 Arrange-Act-Assert 三段式:
1
2
3
4
5
6
7
8
9
10
11
12
| TEST(UserServiceTest, CreateUser_ValidInput_ReturnsTrue) {
// Arrange(准备)
MockDatabase mockDb;
UserService service(mockDb);
EXPECT_CALL(mockDb, insert(_, _)).WillOnce(Return(true));
// Act(执行)
bool result = service.createUser("Alice");
// Assert(断言)
EXPECT_TRUE(result);
}
|
常见反模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| ❌ 反模式 1:测试太多东西
一个 TEST 里验证 10 个不同的行为
→ 应该拆成 10 个独立的 TEST
❌ 反模式 2:测试实现细节而非行为
测试内部数据结构、私有方法
→ 应该通过公共接口测试行为
❌ 反模式 3:脆弱测试(Brittle Tests)
改一行实现代码就要改 20 个测试
→ 测试应该关注"做什么"而非"怎么做"
❌ 反模式 4:测试之间有依赖
Test2 依赖 Test1 的执行结果
→ 每个测试必须独立,用 Fixture 确保初始状态
❌ 反模式 5:不测边界情况
只测 add(1,2)=3,不测溢出、负数、零
→ 边界情况往往是 bug 的温床
❌ 反模式 6:Mock 过度
每个依赖都 Mock,测试和实现严重耦合
→ 只 Mock 外部依赖(数据库、网络),内部逻辑用真实对象
|
第九章:高级技巧
类型参数化测试
当你要测试一个模板类对多种类型都正确时:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| template<typename T>
class StackTypedTest : public ::testing::Test {
protected:
Stack<T> stack;
};
// 要测试的类型列表
using TestTypes = ::testing::Types<int, double, std::string>;
TYPED_TEST_SUITE(StackTypedTest, TestTypes);
TYPED_TEST(StackTypedTest, PushAndPop) {
TypeParam value{}; // TypeParam 是当前的类型
this->stack.push(value);
EXPECT_EQ(this->stack.size(), 1);
this->stack.pop();
EXPECT_TRUE(this->stack.empty());
}
// 这一个 TYPED_TEST 会为 int、double、string 各生成一个测试
|
死亡测试
测试代码在某种输入下是否会崩溃(abort、段错误、assert 失败):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| void divideUnsafe(int a, int b) {
assert(b != 0); // release 下不会检查
// 或者直接 a/b 可能段错误
}
TEST(DeathTest, DivideByZeroCrashes) {
// 期望这个函数调用会导致进程终止
EXPECT_DEATH(divideUnsafe(1, 0), ".*");
// 也可以匹配错误信息
EXPECT_DEATH(divideUnsafe(1, 0), "Assertion.*failed");
}
// 命名约定:死亡测试套件名以 DeathTest 结尾
// Google Test 会用 fork 执行,不影响其他测试
|
跳过测试
1
2
3
4
5
6
7
8
9
10
11
12
13
| TEST(FeatureTest, OnlyOnLinux) {
#ifdef _WIN32
GTEST_SKIP() << "This test only runs on Linux";
#endif
// Linux-only 测试逻辑
}
TEST(FeatureTest, NeedsDatabase) {
if (!isDatabaseAvailable()) {
GTEST_SKIP() << "Database not available";
}
// 需要数据库的测试
}
|
全局 SetUp/TearDown
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class GlobalEnvironment : public ::testing::Environment {
public:
void SetUp() override {
// 所有测试之前执行一次(初始化日志、数据库连接池等)
Logger::init();
}
void TearDown() override {
// 所有测试之后执行一次
Logger::shutdown();
}
};
// 在 main 中注册(如果你用 gtest_main 就不需要自己写 main)
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
::testing::AddGlobalTestEnvironment(new GlobalEnvironment);
return RUN_ALL_TESTS();
}
|
自定义 Matcher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 自定义一个检查"是偶数"的 Matcher
MATCHER(IsEven, "is even") {
return arg % 2 == 0;
}
// 带参数的 Matcher
MATCHER_P(IsDivisibleBy, n, "is divisible by " + std::to_string(n)) {
return arg % n == 0;
}
TEST(CustomMatcherTest, Usage) {
EXPECT_THAT(42, IsEven());
EXPECT_THAT(15, IsDivisibleBy(3));
EXPECT_THAT(15, IsDivisibleBy(5));
// 组合使用
std::vector<int> nums = {2, 4, 6, 8};
EXPECT_THAT(nums, Each(IsEven()));
}
|
第十章:CMake + CTest 集成
完整的 CMake 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| cmake_minimum_required(VERSION 3.14)
project(my_project VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── 主项目 ──
add_library(core
src/calculator.cpp
src/user_service.cpp
src/cache.cpp
)
target_include_directories(core PUBLIC include)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)
# ── 测试(可选构建)──
option(BUILD_TESTS "Build tests" ON)
if(BUILD_TESTS)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
)
# Windows: 防止覆盖父项目的编译器/链接器设置
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
# 多个测试可执行文件
add_executable(test_calculator tests/test_calculator.cpp)
target_link_libraries(test_calculator PRIVATE core GTest::gtest_main GTest::gmock)
add_executable(test_user_service tests/test_user_service.cpp)
target_link_libraries(test_user_service PRIVATE core GTest::gtest_main GTest::gmock)
include(GoogleTest)
gtest_discover_tests(test_calculator)
gtest_discover_tests(test_user_service)
endif()
|
运行和过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| # 构建
cmake -B build -DBUILD_TESTS=ON
cmake --build build
# 运行所有测试
cd build && ctest
# 详细输出
ctest --verbose
ctest -V
# 只运行失败的测试
ctest --rerun-failed
# 过滤测试(CTest 正则)
ctest -R "Calculator" # 名字包含 Calculator 的测试
ctest -E "DeathTest" # 排除死亡测试
# 直接运行可执行文件(更多过滤选项)
./test_calculator --gtest_filter="CalculatorTest.Add*"
./test_calculator --gtest_filter="*Divide*"
./test_calculator --gtest_filter="-*Slow*" # 排除
./test_calculator --gtest_filter="Suite1.*:Suite2.*" # 多个
./test_calculator --gtest_list_tests # 列出所有测试
# 重复运行(检测偶发失败)
./test_calculator --gtest_repeat=100
./test_calculator --gtest_repeat=100 --gtest_break_on_failure
# 随机顺序(检测测试间依赖)
./test_calculator --gtest_shuffle
# 输出 XML 报告(CI 用)
./test_calculator --gtest_output=xml:report.xml
|
Windows 特别注意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Windows 上使用 Google Test 的常见坑
# 1. CRT 链接冲突
# 如果不设置这个,可能报 LNK2038 错误
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
# 2. MSVC 的 UTF-8 支持
if(MSVC)
add_compile_options(/utf-8) # 源码用 UTF-8
endif()
# 3. 在 Visual Studio 中运行
# 可以使用 Test Explorer 窗口直接看到所有测试
# 需要安装 "Google Test Adapter" 扩展
|
第十一章:CI/CD 中的测试
GitHub Actions 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # .github/workflows/test.yml
name: C++ Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -B build -DBUILD_TESTS=ON
- name: Build
run: cmake --build build
- name: Test
run: cd build && ctest --verbose --output-on-failure
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: build/report.xml
|
多平台 CI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
build_type: [Debug, Release]
runs-on: $\{\{ matrix.os \}\}
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -B build -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=$\{\{ matrix.build_type \}\}
- name: Build
run: cmake --build build --config $\{\{ matrix.build_type \}\}
- name: Test
run: cd build && ctest -C $\{\{ matrix.build_type \}\} --verbose
|
第十二章:AI 辅助编程的测试策略
让 AI 帮你写测试
1
2
3
4
5
6
7
8
9
10
11
| 你可以这样指示 AI:
"给这个函数写测试,包括:
1. 正常输入的 happy path
2. 空输入
3. 边界值(最大值、最小值、零)
4. 异常路径(无效输入应该抛什么异常)
5. 性能相关的边界(超大输入)"
AI 擅长这个——它能系统地覆盖边界情况
但你要审查 AI 写的测试是否真的测了有意义的东西
|
让 AI 根据你的测试写实现
1
2
3
4
5
6
7
8
9
10
| 更好的工作流:
1. 你定义接口(.h 文件)
2. 你写测试(或让 AI 帮写,但你要审查)
3. 让 AI 写实现
4. 跑测试
5. 不通过 → 把测试结果给 AI → 让它修改
这样你始终掌控"什么是正确的"(测试)
AI 只负责"怎么实现"(代码)
|
测试即文档
1
2
3
4
5
6
7
8
| 好的测试是最好的文档,因为它:
├── 展示了函数的正确用法
├── 说明了预期的行为和边界
├── 随代码一起更新(不像注释会过时)
└── 可运行、可验证
AI 可以从你的测试中"读懂"你要什么
比自然语言描述更精确
|
实际工作流示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| 场景:你要实现一个 JSON 解析器
第 1 步:你写接口
class JsonParser {
public:
JsonValue parse(const std::string& json);
};
第 2 步:你写(或审查 AI 写的)测试
TEST(JsonParser, ParseEmptyObject) { ... }
TEST(JsonParser, ParseString) { ... }
TEST(JsonParser, ParseNumber) { ... }
TEST(JsonParser, ParseNested) { ... }
TEST(JsonParser, InvalidJson_Throws) { ... }
第 3 步:把头文件 + 测试文件给 AI
"请实现 JsonParser,让所有测试通过"
第 4 步:跑测试
$ ./test_json_parser
[ PASSED ] 4 tests.
[ FAILED ] 1 test.
JsonParser.ParseNested: Expected nested value...
第 5 步:把失败信息给 AI
"ParseNested 测试失败了,这是错误信息,请修复"
第 6 步:修复后全绿
[ PASSED ] 5 tests.
✅ 完成
|
第十三章:实战项目 —— 完整示例
把前面学到的所有技巧串起来,写一个完整的测试案例:
被测代码:简单的任务队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // include/task_queue.h
#pragma once
#include <queue>
#include <mutex>
#include <condition_variable>
#include <optional>
#include <functional>
#include <chrono>
class TaskQueue {
public:
using Task = std::function<void()>;
explicit TaskQueue(size_t max_size = 100);
// 添加任务,队列满时返回 false
bool push(Task task);
// 获取任务,队列空时阻塞等待,超时返回 nullopt
std::optional<Task> pop(std::chrono::milliseconds timeout);
// 获取当前队列大小
size_t size() const;
// 队列是否为空
bool empty() const;
// 关闭队列(pop 将不再阻塞,返回 nullopt)
void shutdown();
bool is_shutdown() const;
private:
mutable std::mutex mutex_;
std::condition_variable cv_;
std::queue<Task> queue_;
size_t max_size_;
bool shutdown_ = false;
};
|
完整测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
| // tests/test_task_queue.cpp
#include <gtest/gtest.h>
#include "task_queue.h"
#include <thread>
#include <atomic>
// ── Fixture ──
class TaskQueueTest : public ::testing::Test {
protected:
TaskQueue queue{5}; // 容量为 5
};
// ── 基本功能 ──
TEST_F(TaskQueueTest, NewQueueIsEmpty) {
EXPECT_TRUE(queue.empty());
EXPECT_EQ(queue.size(), 0);
}
TEST_F(TaskQueueTest, PushIncreasesSize) {
queue.push([]{ });
EXPECT_EQ(queue.size(), 1);
EXPECT_FALSE(queue.empty());
}
TEST_F(TaskQueueTest, PopReturnsTask) {
bool executed = false;
queue.push([&]{ executed = true; });
auto task = queue.pop(std::chrono::milliseconds(100));
ASSERT_TRUE(task.has_value());
task.value()(); // 执行任务
EXPECT_TRUE(executed);
EXPECT_TRUE(queue.empty());
}
TEST_F(TaskQueueTest, FIFO_Order) {
std::vector<int> order;
queue.push([&]{ order.push_back(1); });
queue.push([&]{ order.push_back(2); });
queue.push([&]{ order.push_back(3); });
for (int i = 0; i < 3; i++) {
auto task = queue.pop(std::chrono::milliseconds(100));
ASSERT_TRUE(task.has_value());
task.value()();
}
EXPECT_EQ(order, (std::vector<int>{1, 2, 3}));
}
// ── 边界情况 ──
TEST_F(TaskQueueTest, PushReturnsFalseWhenFull) {
for (int i = 0; i < 5; i++) {
EXPECT_TRUE(queue.push([]{ }));
}
// 第 6 个应该失败
EXPECT_FALSE(queue.push([]{ }));
}
TEST_F(TaskQueueTest, PopTimesOutWhenEmpty) {
auto start = std::chrono::steady_clock::now();
auto task = queue.pop(std::chrono::milliseconds(50));
auto elapsed = std::chrono::steady_clock::now() - start;
EXPECT_FALSE(task.has_value());
EXPECT_GE(elapsed, std::chrono::milliseconds(40)); // 允许少量误差
}
// ── 多线程 ──
TEST_F(TaskQueueTest, ConcurrentPushAndPop) {
std::atomic<int> sum{0};
const int num_tasks = 100;
TaskQueue bigQueue{200};
// 生产者线程
std::thread producer([&]{
for (int i = 1; i <= num_tasks; i++) {
bigQueue.push([&sum, i]{ sum += i; });
}
});
// 消费者线程
std::thread consumer([&]{
for (int i = 0; i < num_tasks; i++) {
auto task = bigQueue.pop(std::chrono::milliseconds(1000));
if (task) task.value()();
}
});
producer.join();
consumer.join();
// 1+2+...+100 = 5050
EXPECT_EQ(sum.load(), 5050);
}
// ── Shutdown ──
TEST_F(TaskQueueTest, ShutdownUnblocksWaitingPop) {
std::optional<TaskQueue::Task> result;
std::thread waiter([&]{
result = queue.pop(std::chrono::milliseconds(5000));
});
// 给 waiter 一点时间进入等待
std::this_thread::sleep_for(std::chrono::milliseconds(50));
queue.shutdown();
waiter.join();
EXPECT_FALSE(result.has_value());
EXPECT_TRUE(queue.is_shutdown());
}
TEST_F(TaskQueueTest, PushFailsAfterShutdown) {
queue.shutdown();
EXPECT_FALSE(queue.push([]{ }));
}
|
对应的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| // src/task_queue.cpp
#include "task_queue.h"
TaskQueue::TaskQueue(size_t max_size) : max_size_(max_size) {}
bool TaskQueue::push(Task task) {
std::lock_guard<std::mutex> lock(mutex_);
if (shutdown_ || queue_.size() >= max_size_) return false;
queue_.push(std::move(task));
cv_.notify_one();
return true;
}
std::optional<TaskQueue::Task> TaskQueue::pop(std::chrono::milliseconds timeout) {
std::unique_lock<std::mutex> lock(mutex_);
if (!cv_.wait_for(lock, timeout, [this]{
return !queue_.empty() || shutdown_;
})) {
return std::nullopt; // 超时
}
if (queue_.empty()) return std::nullopt; // shutdown 且队列空
Task task = std::move(queue_.front());
queue_.pop();
return task;
}
size_t TaskQueue::size() const {
std::lock_guard<std::mutex> lock(mutex_);
return queue_.size();
}
bool TaskQueue::empty() const {
std::lock_guard<std::mutex> lock(mutex_);
return queue_.empty();
}
void TaskQueue::shutdown() {
std::lock_guard<std::mutex> lock(mutex_);
shutdown_ = true;
cv_.notify_all();
}
bool TaskQueue::is_shutdown() const {
std::lock_guard<std::mutex> lock(mutex_);
return shutdown_;
}
|
速查表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 安装 CMake FetchContent + GTest::gtest_main
断言 EXPECT_*(继续)/ ASSERT_*(终止)
浮点数 EXPECT_NEAR / EXPECT_DOUBLE_EQ(不用 EXPECT_EQ)
Fixture 继承 testing::Test + SetUp/TearDown + TEST_F
参数化 TestWithParam<T> + TEST_P + INSTANTIATE_TEST_SUITE_P
Mock MOCK_METHOD + EXPECT_CALL + Return/Throw
匹配器 _(任意)/ Eq / HasSubstr / Contains / AllOf
TDD 循环 红(失败测试) → 绿(最少代码) → 重构(改善结构)
运行过滤 --gtest_filter="Suite.Test*"
输出报告 --gtest_output=xml:report.xml
跳过测试 GTEST_SKIP()
死亡测试 EXPECT_DEATH(statement, regex)
CI cmake --build + ctest --verbose
Windows gtest_force_shared_crt ON + /utf-8
AI 工作流 先写测试 → AI 写实现 → 跑测试 → 迭代
|
测试不是负担,而是保障。尤其在 AI 辅助编程时代,测试是你对代码质量唯一可靠的防线。先写测试,让 AI 去填实现——你掌控标准,它负责干活。
本系列其他文章: