【c++面向对象编程】第49篇:面向对象的单元测试:用GoogleTest测试类

目录

一、为什么需要单元测试?

[二、第一个测试用例:TEST 宏](#二、第一个测试用例:TEST 宏)

[三、常用断言:EXPECT_* vs ASSERT_*](#三、常用断言:EXPECT_* vs ASSERT_*)

常用断言宏

[四、测试夹具(Test Fixture):复用对象](#四、测试夹具(Test Fixture):复用对象)

[五、Mock 对象:解决外部依赖](#五、Mock 对象:解决外部依赖)

[EXPECT_CALL 常用语法](#EXPECT_CALL 常用语法)

六、完整案例:测试一个智能指针类

七、常见错误与最佳实践

错误1:在测试之间共享可变状态

[错误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() → 测试失败
}

最佳实践

  1. 至少覆盖每个代码路径:函数有多少分支,就有多少个测试

  2. 测试边界条件:0、负数、空容器、最大容量

  3. 命名清晰TEST(ClassName, MethodName_Scenario_ExpectedResult)

  4. 一个测试只验证一件事 :多个 EXPECT 可以,但应聚焦一个行为

  5. 用 Mock 隔离外部依赖:不真的访问数据库、网络、文件系统

八、这一篇的收获

你现在应该能:

  • TEST 宏编写基础测试用例

  • 区分 EXPECT_*(失败继续)和 ASSERT_*(失败终止)

  • TEST_F + 夹具类复用初始化和清理代码

  • MOCK_METHOD + EXPECT_CALL 模拟依赖对象

  • 避免测试之间的状态污染和资源泄漏

💡 小作业:为第26-32篇中实现的 DynamicArray 类编写完整测试,覆盖:构造/析构、push_backpop_backoperator[] 边界检查、拷贝构造、移动构造。

下一篇预告:第50篇《从OOP到数据导向设计:现代C++的性能反思》------本系列最后一篇。OOP 的封装、继承、多态带来了良好的抽象,但也带来了性能问题(虚函数开销、缓存不友好)。ECS 架构和 Data-Oriented Design 提供了新思路。下篇总结。

相关推荐
lly2024069 小时前
Python3 条件控制
开发语言
biter down9 小时前
3:GUI⾃动化简单⽰例
开发语言
坚定学代码9 小时前
如何在c++中使用MySQL
开发语言·c++·mysql
小牛蛋9 小时前
vcpkg 管理 PCL + VTK + Qt 开发三维点云可视化软件
开发语言·qt
zandy101110 小时前
2026 BI平台安全治理体系构建:从权限模型到零信任架构
java·开发语言
纽扣66710 小时前
【C++通关之路】C++ 继承深度全景指南:从语法陷阱到内存底层的终极复习
开发语言·c++
wjs202410 小时前
Eclipse 快捷键
开发语言
楼田莉子10 小时前
C++17特性:强制省略拷贝优化/折叠表达式/非类型模板参数/嵌套命名空间
开发语言·c++
froginwe1110 小时前
JavaScript JSON
开发语言