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 时补充回归测试,确保同类问题不会再次出现。
- 对慢测试进行分组,避免影响本地快速反馈。