Skip to content

GoogleTest 在项目工程中的应用

把 GoogleTest 用好,关键不是“测试文件能编译运行”,而是让测试成为工程结构的一部分。测试代码应当有清晰边界、稳定入口、可自动执行、可在失败时快速定位。

推荐工程结构

project
  CMakeLists.txt
  src
    calculator.cpp
  include
    calculator.h
  tests
    CMakeLists.txt
    calculator_test.cpp
  cmake
    Coverage.cmake

被测代码与测试代码分离

推荐把核心逻辑做成库,再让业务程序和测试程序分别链接它。

add_library(calculator_lib
    src/calculator.cpp)

target_include_directories(calculator_lib
    PUBLIC include)

add_executable(app
    src/main.cpp)

target_link_libraries(app
    PRIVATE calculator_lib)

add_executable(unit_tests
    tests/calculator_test.cpp)

target_link_libraries(unit_tests
    PRIVATE calculator_lib GTest::gtest_main)

这样测试目标不需要链接完整应用,也不需要通过命令行驱动外层程序,测试速度更快,失败定位更直接。

示例:测试一个类

class RingBuffer {
public:
    explicit RingBuffer(std::size_t capacity) : data_(capacity) {
    }

    bool Push(int value) {
        if (size_ == data_.size()) {
            return false;
        }
        data_[(head_ + size_) % data_.size()] = value;
        ++size_;
        return true;
    }

    std::optional<int> Pop() {
        if (size_ == 0) {
            return std::nullopt;
        }
        int value = data_[head_];
        head_ = (head_ + 1) % data_.size();
        --size_;
        return value;
    }

private:
    std::vector<int> data_;
    std::size_t head_ = 0;
    std::size_t size_ = 0;
};

TEST(RingBufferTest, PopsInFifoOrder) {
    RingBuffer buffer(2);

    ASSERT_TRUE(buffer.Push(10));
    ASSERT_TRUE(buffer.Push(20));

    EXPECT_EQ(buffer.Pop(), 10);
    EXPECT_EQ(buffer.Pop(), 20);
}

TEST(RingBufferTest, RejectsPushWhenFull) {
    RingBuffer buffer(1);

    ASSERT_TRUE(buffer.Push(10));
    EXPECT_FALSE(buffer.Push(20));
}

测试分层

层次 目标 特点
单元测试 验证单个函数或类 快、稳定、定位准确
组件测试 验证多个模块协作 需要少量真实依赖或替身
集成测试 验证外部系统交互 慢、依赖环境、适合 CI 分组
回归测试 固定曾经出现过的问题 命名应体现缺陷场景

GoogleTest 最适合单元测试和组件测试。集成测试也可以使用 GoogleTest,但应通过标签、过滤器或独立目标和普通单元测试区分。

测试隔离

测试之间必须相互独立。常见隔离原则如下:

  • 不依赖执行顺序。
  • 不共享可变全局状态。
  • 不复用同一个临时文件路径。
  • 不访问真实生产数据库。
  • 不依赖网络、时间、随机数等不稳定因素,除非这些因素被显式控制。

临时文件

测试需要文件时,推荐使用唯一临时目录,并在结束后清理。

#include <filesystem>
#include <fstream>
#include <gtest/gtest.h>

TEST(FileTest, ReadsWrittenContent) {
    const auto path = std::filesystem::temp_directory_path() / "gtest_note_sample.txt";

    {
        std::ofstream output(path);
        output << "hello";
    }

    std::ifstream input(path);
    std::string text;
    input >> text;

    EXPECT_EQ(text, "hello");
    std::filesystem::remove(path);
}

真实工程中应避免固定文件名冲突,可以使用测试名、进程号或随机 UUID 构造路径。

可测试性设计

难测试的代码通常不是因为 GoogleTest 不够强,而是设计把依赖写死了。例如直接在业务函数里读取系统时间、访问网络、创建线程,会让测试不稳定。

更好的做法是把不稳定依赖抽象成接口:

class Clock {
public:
    virtual ~Clock() = default;
    virtual std::chrono::system_clock::time_point Now() const = 0;
};

class Token {
public:
    explicit Token(const Clock& clock) : clock_(clock) {
    }

    bool IsExpired(std::chrono::system_clock::time_point deadline) const {
        return clock_.Now() >= deadline;
    }

private:
    const Clock& clock_;
};

测试时传入假时钟即可稳定验证时间相关行为。

工程中的测试命名建议

对象 命名建议 示例
测试文件 模块名_test.cpp ring_buffer_test.cpp
测试套件 类名Test 或 模块名Test RingBufferTest
测试名 行为 + 条件 PushFailsWhenFull
参数化实例 数据来源或场景 BoundaryValues

测试维护原则

  • 测试失败时,优先判断是产品缺陷还是测试假设过期。
  • 测试应表达外部可观察行为,少依赖内部私有实现。
  • 修复 bug 时补充回归测试,确保同类问题不会再次出现。
  • 对慢测试进行分组,避免影响本地快速反馈。