目录
[二、第一个测试用例:TEST 宏](#二、第一个测试用例:TEST 宏)
[三、常用断言:EXPECT_* vs ASSERT_*](#三、常用断言:EXPECT_* vs ASSERT_*)
[四、测试夹具(Test Fixture):复用对象](#四、测试夹具(Test Fixture):复用对象)
[五、Mock 对象:解决外部依赖](#五、Mock 对象:解决外部依赖)
[EXPECT_CALL 常用语法](#EXPECT_CALL 常用语法)
[错误2:忘记 TearDown 释放资源](#错误2:忘记 TearDown 释放资源)
[错误3:Mock 期望设置但函数未被调用](#错误3:Mock 期望设置但函数未被调用)
一、为什么需要单元测试?
假设你刚写完一个智能指针类,手动运行了几次,看起来没问题。但代码被其他人调用时,release() 在某种边界条件下会崩溃------你根本不知道。
单元测试的价值:
-
自动化验证每个函数/类的行为
-
修改代码后立即知道是否破坏了原有功能
-
强迫你思考边界条件(空指针、负数、最大容量等)
-
本身就是最好的文档(看测试就知道代码该怎么用)
Google Test 是目前 C++ 最流行的单元测试框架,由 Google 开源,支持丰富的断言、测试夹具、参数化测试等功能。
二、第一个测试用例:TEST 宏
cpp
#include <gtest/gtest.h>
// 待测试的函数
int add(int a, int b) {
return a + b;
}
// 测试用例:TEST(测试套件名, 测试用例名)
TEST(AddTest, PositiveNumbers) {
EXPECT_EQ(3, add(1, 2)); // 期望 1+2 等于 3
EXPECT_EQ(5, add(2, 3));
}
TEST(AddTest, NegativeNumbers) {
EXPECT_EQ(-3, add(-1, -2));
EXPECT_EQ(0, add(-5, 5));
}
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
编译运行(Linux/macOS):
bash
g++ -o test test.cpp -lgtest -lgtest_main -pthread && ./test
输出:
text
[==========] Running 2 tests from 1 test suite.
[ RUN ] AddTest.PositiveNumbers
[ OK ] AddTest.PositiveNumbers
[ RUN ] AddTest.NegativeNumbers
[ OK ] AddTest.NegativeNumbers
[==========] 2 tests from 1 test suite ran.
[ PASSED ] 2 tests.
三、常用断言:EXPECT_* vs ASSERT_*
Google Test 提供两类断言:
| 断言 | 失败时行为 | 适用场景 |
|---|---|---|
EXPECT_* |
记录失败,继续执行 | 希望看到所有失败项 |
ASSERT_* |
立即终止当前测试 | 后续代码依赖该断言结果 |
cpp
TEST(AssertionDemo, ExpectVsAssert) {
int* p = nullptr;
ASSERT_NE(nullptr, p); // 如果 p 是空指针,测试终止,避免下面崩溃
EXPECT_EQ(42, *p); // 只有上面通过才会执行
}
常用断言宏
| 断言 | 作用 |
|---|---|
EXPECT_EQ(expected, actual) |
相等 |
EXPECT_NE(val1, val2) |
不相等 |
EXPECT_LT(val1, val2) |
小于 |
EXPECT_GT(val1, val2) |
大于 |
EXPECT_TRUE(condition) |
条件为真 |
EXPECT_FALSE(condition) |
条件为假 |
EXPECT_STREQ(str1, str2) |
C 字符串相等 |
EXPECT_STRNE(str1, str2) |
C 字符串不相等 |
EXPECT_THROW(statement, exception) |
抛指定异常 |
EXPECT_NO_THROW(statement) |
不抛异常 |
四、测试夹具(Test Fixture):复用对象
多个测试需要相同的初始化代码时,用 TEST_F 宏配合夹具类。
cpp
class BankAccountTest : public ::testing::Test {
protected:
void SetUp() override { // 每个测试前执行
account = new BankAccount("张三", 1000);
}
void TearDown() override { // 每个测试后执行
delete account;
}
BankAccount* account;
};
// 使用 TEST_F,套件名必须是夹具类名
TEST_F(BankAccountTest, DepositIncreasesBalance) {
account->deposit(500);
EXPECT_EQ(1500, account->getBalance());
}
TEST_F(BankAccountTest, WithdrawDecreasesBalance) {
account->withdraw(300);
EXPECT_EQ(700, account->getBalance());
}
TEST_F(BankAccountTest, WithdrawMoreThanBalanceFails) {
EXPECT_FALSE(account->withdraw(2000));
EXPECT_EQ(1000, account->getBalance()); // 余额不变
}
注意 :SetUp()/TearDown() 在每个测试用例前后都会调用,不是整个测试套件只调用一次。
五、Mock 对象:解决外部依赖
当类依赖数据库、网络、文件系统时,单元测试不应真的调用这些组件。Google Mock(gmock)可以创建 Mock 对象来模拟依赖。
cpp
#include <gmock/gmock.h>
// 1. 定义接口
class Database {
public:
virtual ~Database() = default;
virtual bool connect(const string& host) = 0;
virtual bool saveUser(const string& name, int age) = 0;
};
// 2. 生成 Mock 类
class MockDatabase : public Database {
public:
MOCK_METHOD(bool, connect, (const string& host), (override));
MOCK_METHOD(bool, saveUser, (const string& name, int age), (override));
};
// 3. 被测试的类
class UserService {
Database* db;
public:
UserService(Database* d) : db(d) {}
bool registerUser(const string& name, int age) {
if (age < 0 || age > 150) return false;
return db->saveUser(name, age);
}
};
// 4. 测试
TEST(UserServiceTest, RegisterCallsSaveUser) {
MockDatabase mockDb;
UserService service(&mockDb);
// 设置期望:saveUser 被调用一次,参数是 ("张三", 25),返回 true
EXPECT_CALL(mockDb, saveUser("张三", 25))
.Times(1)
.WillOnce(testing::Return(true));
EXPECT_TRUE(service.registerUser("张三", 25));
}
EXPECT_CALL 常用语法
cpp
// 期望调用次数
EXPECT_CALL(mock, foo(5)).Times(1);
EXPECT_CALL(mock, bar()).Times(AtLeast(2));
EXPECT_CALL(mock, baz()).Times(AtMost(3));
// 设置返回值
EXPECT_CALL(mock, getValue()).WillOnce(Return(100));
EXPECT_CALL(mock, compute()).WillOnce(Return(1)).WillOnce(Return(2));
// 参数匹配
EXPECT_CALL(mock, process(Ge(10))); // 参数 >= 10
EXPECT_CALL(mock, handle(StrEq("hello"))); // 字符串相等
EXPECT_CALL(mock, func(_)); // 任意参数
六、完整案例:测试一个智能指针类
cpp
#include <gtest/gtest.h>
#include <memory>
template <typename T>
class SimpleSmartPtr {
T* ptr;
public:
explicit SimpleSmartPtr(T* p = nullptr) : ptr(p) {}
~SimpleSmartPtr() { delete ptr; }
// 禁止拷贝
SimpleSmartPtr(const SimpleSmartPtr&) = delete;
SimpleSmartPtr& operator=(const SimpleSmartPtr&) = delete;
// 支持移动
SimpleSmartPtr(SimpleSmartPtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
T* get() const { return ptr; }
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
explicit operator bool() const { return ptr != nullptr; }
};
// ========== 测试代码 ==========
class SimpleSmartPtrTest : public ::testing::Test {
protected:
void SetUp() override {
rawInt = new int(42);
}
void TearDown() override {
// 注意:如果所有权已转移,rawInt 可能已被 delete
// 这里只做清理,测试中要小心管理
}
int* rawInt;
};
TEST_F(SimpleSmartPtrTest, ConstructorTakesRawPointer) {
SimpleSmartPtr<int> ptr(rawInt);
EXPECT_EQ(42, *ptr);
}
TEST_F(SimpleSmartPtrTest, GetReturnsRawPointer) {
SimpleSmartPtr<int> ptr(rawInt);
EXPECT_EQ(rawInt, ptr.get());
}
TEST_F(SimpleSmartPtrTest, OperatorBoolWorks) {
SimpleSmartPtr<int> ptr1(rawInt);
SimpleSmartPtr<int> ptr2(nullptr);
EXPECT_TRUE(ptr1);
EXPECT_FALSE(ptr2);
}
TEST_F(SimpleSmartPtrTest, ReleaseTransfersOwnership) {
SimpleSmartPtr<int> ptr(rawInt);
int* released = ptr.release();
EXPECT_EQ(nullptr, ptr.get());
EXPECT_EQ(42, *released);
delete released; // 手动释放
}
TEST_F(SimpleSmartPtrTest, ResetDeletesOldAndTakesNew) {
SimpleSmartPtr<int> ptr(new int(100));
ptr.reset(new int(200));
EXPECT_EQ(200, *ptr);
}
TEST_F(SimpleSmartPtrTest, MoveTransfersOwnership) {
SimpleSmartPtr<int> ptr1(new int(100));
SimpleSmartPtr<int> ptr2(std::move(ptr1));
EXPECT_EQ(nullptr, ptr1.get());
EXPECT_EQ(100, *ptr2);
}
七、常见错误与最佳实践
错误1:在测试之间共享可变状态
cpp
// ❌ 错误:静态变量在测试间残留
static int counter = 0;
TEST(CounterTest, First) { counter = 1; }
TEST(CounterTest, Second) { EXPECT_EQ(1, counter); } // 依赖执行顺序
// ✅ 正确:每个测试独立初始化
class CounterTest : public ::testing::Test {
protected:
void SetUp() override { counter = 0; }
int counter;
};
错误2:忘记 TearDown 释放资源
cpp
// ❌ 每个测试都 leak 内存
class LeakyTest : public ::testing::Test {
protected:
void SetUp() override { ptr = new int[1000]; }
// 没有 TearDown!
};
// ✅ 在 TearDown 或析构函数中释放
class CleanTest : public ::testing::Test {
protected:
void SetUp() override { ptr = new int[1000]; }
void TearDown() override { delete[] ptr; }
int* ptr;
};
错误3:Mock 期望设置但函数未被调用
cpp
TEST(MockTest, NeverCalled) {
MockDatabase db;
EXPECT_CALL(db, connect("localhost")).Times(1); // 期望调用一次
// 但测试中没调用 db.connect() → 测试失败
}
最佳实践
-
至少覆盖每个代码路径:函数有多少分支,就有多少个测试
-
测试边界条件:0、负数、空容器、最大容量
-
命名清晰 :
TEST(ClassName, MethodName_Scenario_ExpectedResult) -
一个测试只验证一件事 :多个
EXPECT可以,但应聚焦一个行为 -
用 Mock 隔离外部依赖:不真的访问数据库、网络、文件系统
八、这一篇的收获
你现在应该能:
-
用
TEST宏编写基础测试用例 -
区分
EXPECT_*(失败继续)和ASSERT_*(失败终止) -
用
TEST_F+ 夹具类复用初始化和清理代码 -
用
MOCK_METHOD+EXPECT_CALL模拟依赖对象 -
避免测试之间的状态污染和资源泄漏
💡 小作业:为第26-32篇中实现的
DynamicArray类编写完整测试,覆盖:构造/析构、push_back、pop_back、operator[]边界检查、拷贝构造、移动构造。
下一篇预告:第50篇《从OOP到数据导向设计:现代C++的性能反思》------本系列最后一篇。OOP 的封装、继承、多态带来了良好的抽象,但也带来了性能问题(虚函数开销、缓存不友好)。ECS 架构和 Data-Oriented Design 提供了新思路。下篇总结。