GoogleTest 源码分析:参数化死亡测试与调试
参数化测试、类型参数化测试和死亡测试都建立在普通测试注册与运行机制之上。它们看起来更复杂,本质上是扩展了“如何生成测试实例”和“如何执行测试体”。
参数化测试的源码思路
参数化测试比普通测试多了一层“参数生成器”和“实例化”。
class ParserTest : public ::testing::TestWithParam<std::string> {
};
TEST_P(ParserTest, AcceptsValidText) {
EXPECT_FALSE(GetParam().empty());
}
INSTANTIATE_TEST_SUITE_P(
ValidInputs,
ParserTest,
::testing::Values("a", "abc"));
可以从源码机制上理解为:
TEST_P定义一个带参数的测试模式。INSTANTIATE_TEST_SUITE_P提供参数集合。- GoogleTest 根据参数集合生成多个具体测试实例。
- 每个测试实例运行时通过
GetParam()取得当前参数。
运行时名称通常会变成类似:
ValidInputs/ParserTest.AcceptsValidText/0
ValidInputs/ParserTest.AcceptsValidText/1
参数化测试的关键不是复制多份测试代码,而是把“测试逻辑”和“测试数据”分开注册。
类型参数化测试的源码思路
类型参数化测试发生在编译期。它通过模板和类型列表为每个类型生成对应测试。
template <typename T>
class ContainerTest : public ::testing::Test {
};
using TypesToTest = ::testing::Types<int, double>;
TYPED_TEST_SUITE(ContainerTest, TypesToTest);
TYPED_TEST(ContainerTest, DefaultVectorIsEmpty) {
std::vector<TypeParam> values;
EXPECT_TRUE(values.empty());
}
可以理解为编译器生成了 int 和 double 两套测试类,然后 GoogleTest 分别注册它们。值参数化测试偏运行期数据展开,类型参数化测试偏编译期类型展开。
参数命名
参数化测试默认使用数字后缀。如果希望失败信息更清楚,可以提供命名函数:
struct ParseCase {
std::string input;
bool expected_ok;
};
std::string CaseName(const ::testing::TestParamInfo<ParseCase>& info) {
return info.param.expected_ok ? "Valid" : "Invalid";
}
INSTANTIATE_TEST_SUITE_P(
TextCases,
ParserTest,
::testing::Values(ParseCase{"abc", true}, ParseCase{"", false}),
CaseName);
命名函数返回值需要是合法的测试名片段,通常只使用字母、数字和下划线。
死亡测试的运行机制
死亡测试用于验证一段代码会导致进程退出,例如 assert、abort、非法参数触发的终止。它不能简单地在当前测试进程中执行,因为一旦当前进程真的退出,整个测试程序也会结束。
因此死亡测试通常采用类似下面的策略:
父进程
-> 创建子进程或隔离执行环境
-> 子进程执行待验证语句
-> 父进程捕获退出码和 stderr
-> 检查退出行为是否符合预期
-> 检查错误输出是否匹配正则
示例:
void CrashWhenNull(int* pointer) {
assert(pointer != nullptr);
}
TEST(PointerDeathTest, DiesWhenPointerIsNull) {
EXPECT_DEATH(CrashWhenNull(nullptr), "pointer != nullptr");
}
死亡测试的注意点:
- 待测语句会在隔离环境中执行,不要依赖父进程中的复杂共享状态。
- 死亡测试通常比普通单元测试慢。
- 匹配字符串来自标准错误输出,不同平台或编译模式可能不同。
NDEBUG会关闭assert,这会影响基于assert的死亡测试。
死亡测试风格
GoogleTest 支持通过参数控制死亡测试风格:
./unit_tests --gtest_death_test_style=fast
./unit_tests --gtest_death_test_style=threadsafe
| 风格 | 说明 |
|---|---|
fast |
通常更快,但对多线程上下文要求更谨慎 |
threadsafe |
更重视线程安全,代价是更慢 |
如果测试程序中已经创建了线程,死亡测试更容易暴露平台相关问题。此时应尽量让死亡测试独立、简单,并避免依赖复杂全局状态。
调试 GoogleTest 本身或测试注册问题
当测试没有按预期运行时,可以按以下顺序排查:
- 使用
--gtest_list_tests确认测试是否注册。 - 如果没有注册,检查测试源文件是否加入测试目标。
- 如果已注册但不运行,检查
--gtest_filter和DISABLED_前缀。 - 如果运行后崩溃,检查夹具
SetUp、全局环境和静态对象。 - 如果只在批量运行失败,检查共享状态和执行顺序依赖。
- 如果死亡测试异常,检查平台、正则、
assert是否被NDEBUG关闭。
常用调试参数
./unit_tests --gtest_list_tests
./unit_tests --gtest_filter=SuiteName.TestName
./unit_tests --gtest_repeat=100 --gtest_break_on_failure
./unit_tests --gtest_shuffle --gtest_random_seed=123
./unit_tests --gtest_also_run_disabled_tests
| 参数 | 适合排查的问题 |
|---|---|
--gtest_list_tests |
测试是否注册 |
--gtest_filter |
单独复现某个失败 |
--gtest_repeat |
偶现失败 |
--gtest_shuffle |
测试顺序依赖 |
--gtest_random_seed |
固定随机顺序 |
--gtest_break_on_failure |
失败时进入调试器 |
小结
参数化测试关注“一个测试逻辑生成多个实例”,类型参数化测试关注“一个测试逻辑覆盖多个类型”,死亡测试关注“在隔离执行环境中验证异常退出”。它们不是独立体系,而是普通 GoogleTest 注册、运行和结果上报机制的扩展。