在《使用GTest进行单元测试的简明指南》中,我们学习了如何使用 GTest
编写基本的测试用例。
但是在实际项目中,测试需求往往更复杂:多个用例需要相同的初始化、依赖对象难以控制、测试是否足够全面难以评估……
本文将带你实战掌握 GTest
的高级功能:Test Fixture(测试夹具)、Mock(模拟对象) 与 覆盖率分析,帮助你写出更清晰、可维护的测试代码。
核心概念简述
Test Fixture(测试夹具)
当多个测试共享相同的初始化和清理逻辑时,使用夹具类继承 ::testing::Test
并重载 SetUp()
和 TearDown()
方法可以避免重复代码、提升测试独立性。
典型场景有:
- 测试中多次创建类对象
- 临时文件
SetUp()
会在测试开始时自动运行,TearDown()
则会在测试结束时自动运行。
Mock(模拟对象)
当被测试对象依赖外部模块(如网络、数据库、传感器等)时,Mock 可以替代这些依赖,模拟各种行为和边界情况,隔离测试对象。
常见用途有:
- 模拟返回特定结果;
- 模拟异常或错误行为;
- 验证方法是否被调用,以及调用次数与参数。
代码覆盖率
覆盖率衡量测试对代码的执行程度。
它虽然不能完全代表测试质量,但能有效代码中识别未被测试的路径。
待测对象:Calculator
我们以一个简单的加法器为例,它通过 Validator
对象判断加法是否会导致整数溢出。如果合法就执行累加,否则将结果重置为 0。
核心代码如下(代码仓库在底部):
include/my_math.h
#pragma once
#include <climits>
#include <string>
class Validator {
public:
virtual ~Validator() = default;
virtual bool IsValid(int current, int to_add);
};
class Calculator {
public:
Calculator(Validator* validator);
void Add(int a);
void Reset();
int GetResult();
private:
Validator* validator_;
int result_;
};
src/my_math.cc
#include "my_math.h"
bool Validator::IsValid(int current, int to_add) {
// 检查加法是否会导致溢出
if (to_add > 0) {
return current <= INT_MAX - to_add;
} else {
return current >= INT_MIN - to_add;
}
}
Calculator::Calculator(Validator* validator) : validator_(validator), result_(0) {}
void Calculator::Add(int a) {
if (validator_->IsValid(result_, a)) {
result_ += a; // 未溢出,执行加法
} else {
result_ = 0; // 溢出,重置结果
}
}
void Calculator::Reset() { result_ = 0; }
int Calculator::GetResult() { return result_; }
测试:Test Fixture + Mock
我们将使用测试夹具来管理 Calculator
的创建与销毁,同时使用 MockValidator
来模拟不同的数值判断结果。
测试代码如下:
unit_test/my_math_test.cc
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "my_math.h"
class MockValidator : public Validator {
public:
MOCK_METHOD(bool, IsValid, (int current, int to_add), (override));
};
class CalculatorTest : public ::testing::Test {
protected:
void SetUp() override {
calc_ = new Calculator(&mock_validator_);
calc_->Reset();
}
void TearDown() override {
delete calc_;
calc_ = nullptr;
}
MockValidator mock_validator_;
Calculator* calc_;
};
TEST_F(CalculatorTest, AddWithValidResultMock) {
EXPECT_CALL(mock_validator_, IsValid(0, 100)).WillOnce(::testing::Return(true));
calc_->Add(100);
EXPECT_EQ(calc_->GetResult(), 100);
}
TEST_F(CalculatorTest, AddWithOverflowResultMock) {
EXPECT_CALL(mock_validator_, IsValid(0, INT_MAX)).WillOnce(::testing::Return(false));
calc_->Add(INT_MAX);
EXPECT_EQ(calc_->GetResult(), 0); // 溢出后重置
}
✅ Mock 使用小提示
EXPECT_CALL
用于设定模拟行为,如代码中的:
EXPECT_CALL(mock_validator_, IsValid(0, 100)).WillOnce(Return(true));
这表示模拟一次 IsValid(0, 100)
调用,并返回 true
。
代码覆盖率分析:使用 lcov
步骤一:安装lcov
sudo apt install lcov
步骤二:修改 CMake 配置
CMakeLists.txt(顶层)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 --coverage")
unit_test/CMakeLists.txt
target_link_libraries(math_test
gtest_main
gmock_main
gcov
my_math
)
步骤二:生成覆盖率报告
cd build
cmake ..
make
./unit_test/math_test
# 收集覆盖信息
lcov --capture --directory . --output-file coverage.info
# 过滤不相关路径
lcov --remove coverage.info '/usr/*' '*/third_party/*' '*/unit_test/*' --output-file coverage.info
# 生成 HTML 报告
genhtml coverage.info --output-directory coverage_report
打开 coverage_report/index.html
即可查看覆盖率可视化报告。
报告解析
上图是覆盖率的总览图,我们看src
的报告就可以了。
我们可以看到,函数覆盖率是80%,为什么没有达到100%呢?
点击src
进一步查看:
如图所示,通过2个测试用例,我们成功覆盖了Add
函数的两条分支,因此Add
函数的代码都被执行到了。
但是,我们在测试时,使用的IsValid
函数我们是模拟出来的,所以真实的Isvalid
函数并没有被执行。
所以覆盖率就没有达到100%。
总结
通过使用 Test Fixture 和 Mock,我们实现了一个结构清晰、依赖解耦的测试方案。
代码覆盖率分析则让我们直观的看到测试是否足够全面。
希望这篇文章能帮助你在实际项目中用好 GTest
~
📌 示例代码仓库:
👉 https://github.com/LeafTime/GTestAdvanced
本文首发于微信公众号《Linux在秋名山》,欢迎大家关注~