十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++项目工程化实战:gflags配置管理与gtest单元测试框架详解

C++项目工程化实战:gflags配置管理与gtest单元测试框架详解 1. 项目概述为什么我们需要gflags和gtest在C项目开发里尤其是当你从写“玩具”代码转向构建一个正经的、需要维护和协作的项目时两个问题会立刻变得尖锐起来第一如何优雅地管理那些运行时才确定的配置参数第二如何保证你的代码在修改后依然正确而不是在深夜被一个低级bug叫醒这就是gflags和gtest这两个Google出品的库要帮你解决的核心痛点。gflags全称Google Flags Library它不是一个图形界面库而是一个命令行参数解析库。想象一下你的程序需要配置数据库地址、日志级别、线程池大小。把这些硬编码在代码里每次改配置都要重新编译运维同事会找你“谈心”。自己写argc/argv解析很快你就会陷入参数校验、类型转换、默认值、帮助信息生成的繁琐泥潭。gflags让你用声明式的方式定义参数自动帮你搞定解析、类型检查、生成--help信息让配置管理变得清爽。gtest即Google Test是C的单元测试框架。单元测试不是“可选的奢侈品”而是现代软件工程的“安全带”。它能让你在修改代码后快速验证功能是否正常避免回归错误。更重要的是一套好的测试本身就是最好的文档它清晰地展示了代码应该怎么用、边界条件是什么。gtest提供了丰富的断言宏、测试夹具、死亡测试等机制让编写结构清晰、覆盖全面的测试用例变得非常顺手。把它们俩放在一起介绍是因为在实际项目中它们常常是并肩作战的“黄金搭档”。你的main函数里可能先调用gflags的ParseCommandLineFlags来解析用户输入的配置然后再调用gtest的RUN_ALL_TESTS()来执行测试套件如果你的程序集成了自测试。理解它们是构建一个健壮、可配置、可测试的C项目框架的重要一步。2. gflags深度解析告别手写参数解析的混乱时代2.1 gflags的核心概念与基本用法gflags的核心思想是“全局定义随处可用”。你不需要把参数变量传来传去只需要在合适的源文件中定义它然后在任何需要的地方直接使用这个全局变量即可。gflags会确保它在main函数开始时被正确初始化。定义一个gflags参数非常简单使用宏即可。例如我们想定义一个字符串类型的数据库主机地址一个整型的端口号还有一个布尔型的开关来决定是否开启调试日志// 在某个.cpp文件比如 main.cpp 或专门的 flags.cpp中定义 #include gflags/gflags.h // 定义参数 // DEFINE_类型(参数名, 默认值, 描述性帮助信息) DEFINE_string(db_host, localhost, Database server hostname); DEFINE_int32(db_port, 3306, Database server port); DEFINE_bool(enable_debug_log, false, Enable verbose debug logging);这里有几个关键点需要注意。第一DEFINE_xxx宏实际上定义了一个全局变量。对于DEFINE_string变量名是FLAGS_db_host对于DEFINE_int32是FLAGS_db_port以此类推。第二定义和声明是分开的。如果你需要在其他文件中使用这个变量需要用DECLARE_xxx宏进行声明这通常放在头文件里。// 在另一个.cpp文件或头文件中声明以便使用 DECLARE_string(db_host); DECLARE_int32(db_port); DECLARE_bool(enable_debug_log); void connectToDatabase() { // 直接使用 FLAGS_ 前缀的变量 std::string connection_str FLAGS_db_host : std::to_string(FLAGS_db_port); if (FLAGS_enable_debug_log) { LOG(INFO) Connecting to connection_str; } // ... 实际的连接逻辑 }在main函数中你需要初始化并解析命令行参数int main(int argc, char* argv[]) { // 初始化gflags通常第一个参数是argc的地址第二个是argv gflags::ParseCommandLineFlags(argc, argv, true); // 第三个参数为true时gflags会移除它识别出的参数修改argc和argv。 // 这样剩下的参数就可以被你的程序或其他库如gtest继续处理。 // 现在所有FLAGS_*变量都已经被赋予了命令行传入的值或默认值 std::cout DB Host: FLAGS_db_host std::endl; // ... 你的程序主逻辑 // 程序结束时可以非必须调用这个来释放gflags内部内存 gflags::ShutDownCommandLineFlags(); return 0; }运行程序时用户就可以通过命令行来覆盖默认值./my_program --db_host192.168.1.100 --db_port5432 --enable_debug_logtrue或者使用--help查看所有定义的参数及其说明。注意DEFINE_xxx宏不能放在头文件里除非你把它放在一个命名空间内并且确保每个包含该头文件的编译单元都只想引用同一个变量这很棘手容易导致链接错误。最佳实践是在一个.cpp文件中定义所有参数在对应的头文件中用DECLARE_xxx声明它们或者直接在需要使用的.cpp文件中定义如果不需跨文件共享。2.2 高级特性与配置管理实战除了基础类型gflags还支持更复杂的场景。1. 参数校验器你可以为参数注册一个校验函数在解析完成后立即验证其有效性。例如检查端口号是否在有效范围内#include gflags/gflags.h static bool ValidatePort(const char* flagname, int32_t value) { if (value 0 value 65535) { return true; } printf(Invalid value for --%s: %d. Must be between 1 and 65535.\n, flagname, value); return false; } // 在定义参数后main函数Parse之前注册校验器 DEFINE_int32(port, 8080, Listening port); // 静态全局变量利用构造函数在main之前执行注册 static const bool port_dummy gflags::RegisterFlagValidator(FLAGS_port, ValidatePort);2. 从文件加载配置对于参数很多的项目每次都通过命令行传递很麻烦。gflags允许你从一个文件中加载配置。文件内容就是每行一个--flagvalue的形式。# config.cfg --db_hostproduction-db.internal --db_port3306 --enable_debug_logfalse --log_levelWARNING然后在命令行指定配置文件./my_program --flagfileconfig.cfg你还可以在文件里嵌套--flagfile指令。这个功能对于环境隔离开发、测试、生产特别有用。3. 与环境变量集成虽然gflags本身不直接读取环境变量但你可以很容易地在main函数开始时手动将环境变量设置到FLAGS_*变量中然后再调用ParseCommandLineFlags。命令行参数的优先级最高会覆盖你的预设值。int main(int argc, char* argv[]) { // 先尝试从环境变量读取作为默认值的覆盖 const char* env_db_host std::getenv(APP_DB_HOST); if (env_db_host ! nullptr) { FLAGS_db_host env_db_host; // 直接给FLAGS_变量赋值 } // 再解析命令行命令行参数会覆盖环境变量的设置 gflags::ParseCommandLineFlags(argc, argv, true); // ... }4. 与配置中心结合在现代微服务架构中配置可能来自像etcd、Consul或Apollo这样的配置中心。gflags依然可以作为本地配置的表示层。你可以在程序启动时先从配置中心拉取配置将其转换为命令行参数字符串数组然后传递给gflags::ParseCommandLineFlags。或者更常见的做法是将配置中心的值直接赋给FLAGS_*变量并跳过对某些标志的命令行解析。实操心得标志定义的“命名空间”问题默认情况下所有gflags参数都在全局命名空间里。在一个大型项目中如果不同模块都定义了--log_level就会冲突。gflags提供了“模块化”标志的定义方式通过DEFINE_xxx的变种DEFINE_xxx_in_module较少用或者更简单地通过给标志名增加前缀来模拟命名空间例如--frontend_log_level和--backend_log_level。在代码中访问时使用完整的FLAGS_frontend_log_level即可。清晰的命名约定比复杂的机制更有效。3. gtest单元测试框架为你的代码构建安全网3.1 从零开始编写你的第一个测试让我们暂时忘掉那些复杂的测试概念。单元测试的本质就是给定一些输入调用一个函数或方法检查它的输出或行为是否符合预期。gtest让这个过程标准化、自动化。首先安装gtest。推荐使用vcpkg或conan这样的C包管理器或者直接下载源码编译。以vcpkg为例vcpkg install gtest然后在你的CMakeLists.txt中链接它。假设我们有一个简单的函数要测试这个函数计算斐波那契数列// fibonacci.h #pragma once int Fibonacci(int n);// fibonacci.cpp #include fibonacci.h int Fibonacci(int n) { if (n 1) return n; return Fibonacci(n - 1) Fibonacci(n - 2); }为它编写测试。创建一个新文件fibonacci_test.cpp#include gtest/gtest.h #include fibonacci.h // 定义一个测试用例Test Case名称是FibonacciTest // 在这个用例里可以定义多个测试Test TEST(FibonacciTest, HandlesZeroInput) { EXPECT_EQ(Fibonacci(0), 0); } TEST(FibonacciTest, HandlesPositiveInput) { EXPECT_EQ(Fibonacci(1), 1); EXPECT_EQ(Fibonacci(2), 1); EXPECT_EQ(Fibonacci(3), 2); EXPECT_EQ(Fibonacci(4), 3); EXPECT_EQ(Fibonacci(5), 5); EXPECT_EQ(Fibonacci(10), 55); } // 测试负数输入假设我们期望返回-1表示错误 TEST(FibonacciTest, HandlesNegativeInput) { EXPECT_EQ(Fibonacci(-1), -1); EXPECT_EQ(Fibonacci(-10), -1); }TEST宏是gtest的基础构件。第一个参数是测试用例名第二个是测试名它们共同组成一个唯一的测试标识如FibonacciTest.HandlesZeroInput。EXPECT_EQ是一个断言Assertion它检查两个值是否相等。如果不等测试失败但会继续执行当前测试中的其他断言。与之对应的是ASSERT_EQ如果失败会立刻终止当前测试。编写一个main函数来运行所有测试gtest库提供了一个默认的main但自定义main可以让你在测试前后做一些初始化和清理工作// main_test.cpp #include gtest/gtest.h int main(int argc, char **argv) { // 初始化gtest传入命令行参数gtest自己也支持一些命令行参数如--gtest_filter ::testing::InitGoogleTest(argc, argv); // 运行所有测试并返回结果 return RUN_ALL_TESTS(); }编译并运行测试g -stdc11 fibonacci.cpp fibonacci_test.cpp main_test.cpp -lgtest -lgtest_main -pthread -o fibonacci_test ./fibonacci_test如果一切正常你会看到类似这样的输出[] Running 3 tests from 1 test suite. [----------] Global test environment set-up. [----------] 3 tests from FibonacciTest [ RUN ] FibonacciTest.HandlesZeroInput [ OK ] FibonacciTest.HandlesZeroInput (0 ms) [ RUN ] FibonacciTest.HandlesPositiveInput [ OK ] FibonacciTest.HandlesPositiveInput (0 ms) [ RUN ] FibonacciTest.HandlesNegativeInput [ OK ] FibonacciTest.HandlesNegativeInput (0 ms) [----------] 3 tests from FibonacciTest (0 ms total) [----------] Global test environment tear-down. [] 3 tests from 1 test suite ran. (1 ms total) [ PASSED ] 3 tests.3.2 测试夹具Test Fixture与更复杂的测试场景当多个测试需要相同的设置Setup和清理Teardown代码时比如都需要构造一个特定的对象或准备一些数据使用测试夹具可以避免代码重复。夹具就是一个类继承自::testing::Test。假设我们有一个简单的StringBuilder类需要测试// string_builder.h #include string #include vector class StringBuilder { public: void Append(const std::string str) { parts_.push_back(str); } std::string ToString() const { std::string result; for (const auto part : parts_) { result part; } return result; } void Clear() { parts_.clear(); } private: std::vectorstd::string parts_; };为它创建测试夹具// string_builder_test.cpp #include gtest/gtest.h #include string_builder.h // 定义测试夹具类 class StringBuilderTest : public ::testing::Test { protected: // 每个测试开始前都会执行的函数 void SetUp() override { // 在这里初始化测试夹具的成员变量 builder.Append(Hello); builder.Append(, ); } // 每个测试结束后都会执行的函数如果资源需要清理 void TearDown() override { // 通常如果SetUp中分配了动态内存在这里释放 // 对于StringBuilderClear可能不是必须的因为每个测试都是新的对象 // builder.Clear(); } // 所有测试都可以访问的成员变量 StringBuilder builder; }; // 使用 TEST_F 宏来编写使用夹具的测试。第一个参数是夹具类名。 TEST_F(StringBuilderTest, AppendAndToString) { // 此时builder已经经过了SetUp包含了Hello, builder.Append(World!); EXPECT_EQ(builder.ToString(), Hello, World!); } TEST_F(StringBuilderTest, ClearResetsBuilder) { // 这个测试开始时builder同样包含Hello, EXPECT_FALSE(builder.ToString().empty()); builder.Clear(); EXPECT_TRUE(builder.ToString().empty()); // 可以继续追加 builder.Append(New Start); EXPECT_EQ(builder.ToString(), New Start); }TEST_F中的F代表Fixture。关键点在于对于每个TEST_Fgtest都会创建一个全新的StringBuilderTest对象。这意味着SetUp和TearDown会在每个测试前后被调用测试之间是隔离的一个测试对builder的修改不会影响另一个测试。这是编写稳定、可重复测试的基石。高级断言与匹配器gtest提供了丰富的断言除了EXPECT_EQ/ASSERT_EQ还有EXPECT_TRUE(condition)/EXPECT_FALSE(condition)EXPECT_STREQ(str1, str2)C字符串比较EXPECT_THROW(statement, exception_type)期望抛出特定异常EXPECT_NEAR(val1, val2, abs_error)浮点数近似相等更强大的是EXPECT_THAT和匹配器Matchers它们提供了更富表达力的断言方式需要包含gmock/gmock.hGoogle Mock是gtest的姊妹库提供模拟对象功能也包含匹配器。#include gmock/gmock.h using ::testing::StartsWith; using ::testing::HasSubstr; TEST(StringBuilderTest, WithMatchers) { StringBuilder builder; builder.Append(Error: File not found); // 使用匹配器检查字符串是否以特定前缀开头 EXPECT_THAT(builder.ToString(), StartsWith(Error:)); // 检查是否包含子串 EXPECT_THAT(builder.ToString(), HasSubstr(not found)); }4. gflags与gtest的协同工作与项目集成4.1 在测试中使用gflags配置这是一个非常常见的场景你的测试可能需要不同的配置来运行。例如数据库连接的测试需要指定测试数据库的地址和端口。你可以直接在测试中使用gflags变量。首先确保在测试代码中DECLARE或DEFINE了需要的标志。然后你可以在测试夹具的SetUp中或者直接在测试函数中读取或修改它们。// 假设在项目的某个地方定义了 --test_db_host 和 --test_db_port DECLARE_string(test_db_host); DECLARE_int32(test_db_port); class DatabaseTest : public ::testing::Test { protected: void SetUp() override { // 使用gflags变量来建立测试数据库连接 connection_ Connect(FLAGS_test_db_host, FLAGS_test_db_port); ASSERT_TRUE(connection_-IsOk()) Failed to connect to test DB at FLAGS_test_db_host : FLAGS_test_db_port; } std::unique_ptrDatabaseConnection connection_; }; TEST_F(DatabaseTest, InsertAndQuery) { // 使用connection_进行测试... }运行测试时通过命令行传递参数./database_test --test_db_hosttest.local --test_db_port3307 --gtest_filterDatabaseTest.*这里有一个极其重要的细节也是开头引用的那段2010年Google Groups讨论的核心gflags和gtest命令行参数解析的顺序。gtest自己也接受命令行参数如--gtest_filter用来过滤要运行的测试用例。如果先调用gflags::ParseCommandLineFlags并且将第三个参数设为truegflags会把它认识的参数如--test_db_host从argv中移除。那么剩下的参数可能包含--gtest_filter再传递给::testing::InitGoogleTestgtest就能正确识别自己的参数了。所以正确的main函数顺序是int main(int argc, char* argv[]) { // 1. 先让gflags解析并移除它认识的参数 gflags::ParseCommandLineFlags(argc, argv, true); // 此时argv中只剩下gtest的参数和你的程序可能需要的其他参数 // 2. 初始化gtest让它解析剩下的参数 ::testing::InitGoogleTest(argc, argv); // 3. 运行测试 return RUN_ALL_TESTS(); }如果顺序反过来先初始化gtest再解析gflagsgflags可能会重排argv导致gtest无法正确识别原本属于它的参数从而引发错误。这就是那段古老讨论中Zhanyong Wan和Prashant在讨论的问题。虽然现在的版本兼容性可能更好但遵循这个顺序是最保险的做法。4.2 构建系统的集成以CMake为例一个现代C项目通常使用CMake管理构建。将gflags和gtest集成进去是标准操作。首先找到这些库。你可以用find_package如果系统已安装或者更好的方式使用FetchContent或add_subdirectory直接包含它们的源码这样能确保版本一致便于团队协作。使用FetchContentCMake 3.11推荐cmake_minimum_required(VERSION 3.14) project(MyCppProject) set(CMAKE_CXX_STANDARD 11) # 获取 gflags include(FetchContent) FetchContent_Declare( gflags GIT_REPOSITORY https://github.com/gflags/gflags.git GIT_TAG v2.2.2 # 指定一个稳定版本 ) FetchContent_MakeAvailable(gflags) # 获取 googletest (包含gtest和gmock) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.11.0 ) FetchContent_MakeAvailable(googletest) # 你的主程序 add_executable(my_app main.cpp src/fibonacci.cpp src/string_builder.cpp) target_link_libraries(my_app PRIVATE gflags) # 你的测试程序 add_executable(fibonacci_test tests/fibonacci_test.cpp src/fibonacci.cpp) target_link_libraries(fibonacci_test PRIVATE gtest gtest_main) # 如果需要gmock的匹配器链接gmock # target_link_libraries(fibonacci_test PRIVATE gtest gmock gtest_main) add_executable(string_builder_test tests/string_builder_test.cpp src/string_builder.cpp) target_link_libraries(string_builder_test PRIVATE gtest gtest_main) # 添加一个方便的“test”目标来运行所有测试 enable_testing() add_test(NAME FibonacciTest COMMAND fibonacci_test) add_test(NAME StringBuilderTest COMMAND string_builder_test)然后在构建目录下cmake -B build -S . cmake --build build cd build ctest --output-on-failure # 运行所有测试并显示失败详情实操心得处理Windows下的动态链接在Windows上使用gtest时默认可能是动态链接.dll。如果你的测试程序是动态链接到gtest的需要确保运行时能找到gtest.dll。要么将其路径加入PATH环境变量要么使用静态链接。在CMake中可以在获取googletest前设置选项set(INSTALL_GTEST OFF CACHE BOOL FORCE) # 通常不需要安装 set(BUILD_SHARED_LIBS OFF CACHE BOOL FORCE) # 强制静态编译 FetchContent_Declare(...)静态链接可以避免运行时依赖问题但会增大你的可执行文件体积。根据项目需求权衡。5. 常见问题排查与性能调优实战5.1 gflags和gtest的典型“坑”与解决方案问题1链接错误 - “undefined reference toFLAGS_xxx”这是最常见的问题。原因是你只在A.cpp中用DEFINE_string(flag_name, ...)定义了标志在B.cpp中使用DECLARE_string(flag_name)声明但B.cpp在链接时找不到定义。检查1确保包含DECLARE_string的B.cpp文件在编译时链接了定义该标志的A.cpp所在的目标文件或库。检查2确保没有在头文件里使用DEFINE_string。如果多个.cpp文件包含了这个头文件会导致多重定义。坚持“在.cpp中定义在.h中声明”的原则。检查3对于bool类型标志访问时是FLAGS_flag_name而不是FLAGS_flag_name()。问题2gtest测试发现不了No tests found运行测试程序输出[] 0 tests from 0 test suites ran.。检查1测试代码是否被正确编译链接确保包含TEST或TEST_F宏的.cpp文件被添加到了add_executable中。检查2测试程序的主函数是否正确如果你自己写了main确保调用了RUN_ALL_TESTS()。如果使用gtest_main库提供的默认main则在链接时需要-lgtest_main并且不要自己定义main函数。检查3是否使用了--gtest_filter过滤掉了所有测试尝试不加过滤器运行。问题3测试顺序不确定导致状态污染虽然gtest默认测试顺序不确定但有时我们希望测试按顺序执行例如集成测试有依赖。虽然不推荐测试之间有依赖但如果必须可以设置// 在main函数中InitGoogleTest之后 ::testing::GTEST_FLAG(shuffle) false; // 关闭随机排序 // 或者更精细地控制--gtest_shuffle命令行参数更好的做法是使用测试夹具的SetUp和TearDown确保每个测试的独立性彻底消除顺序依赖。问题4gflags参数在动态库中定义主程序无法识别如果你的项目结构包含动态库.so或.dll并且在动态库中定义了gflags参数主程序或其他库可能无法看到它们。这是因为gflags的标志注册是进程全局的但动态库的符号可见性可能受限。解决方案在主程序或一个公共初始化模块中集中定义所有跨模块使用的标志。如果必须在动态库中定义确保该库被链接到主程序并且在主程序启动时该动态库的初始化代码包含DEFINE_xxx的全局变量初始化会被执行。在Linux下确保链接时使用了-Wl,--export-dynamic或类似选项或者使用dlopen时指定RTLD_GLOBAL标志。5.2 测试性能优化与最佳实践1. 测试分类与选择性执行单元测试应该很快。但项目中可能还有集成测试、端到端测试它们比较慢。使用gtest的“测试套件”标签功能来分类// 定义一个“慢速”测试类别 TEST(DatabaseIntegrationTest, ComplexQuery) { // ... 耗时的数据库操作 } // 或者使用自定义的“类型化测试”或“参数化测试”来组织但更简单的是用命名约定。然后在CMake中创建不同的测试目标或者通过--gtest_filter来运行特定测试# 只运行快速单元测试 ./my_tests --gtest_filter*-*IntegrationTest* # 或者反过来只运行集成测试 ./my_tests --gtest_filter*IntegrationTest*2. 模拟Mock与依赖注入单元测试的核心是“隔离”。如果你的代码依赖一个慢速或不可控的外部服务如网络、数据库直接测试会很痛苦。这时应该使用“模拟对象”Mock来替代真实依赖。gtest的姊妹库Google Mockgmock就是干这个的。假设你的业务逻辑依赖一个HttpClientclass HttpClient { public: virtual ~HttpClient() default; virtual std::string Get(const std::string url) 0; // 纯虚函数便于模拟 }; class MyService { public: MyService(HttpClient* client) : client_(client) {} bool Process() { std::string data client_-Get(http://api.example.com/data); return !data.empty(); } private: HttpClient* client_; };在测试中你可以创建一个MockHttpClient#include gmock/gmock.h class MockHttpClient : public HttpClient { public: MOCK_METHOD(std::string, Get, (const std::string url), (override)); }; TEST(MyServiceTest, ProcessSucceedsOnNonEmptyResponse) { MockHttpClient mock_client; MyService service(mock_client); // 设置期望当Get被调用时返回hello EXPECT_CALL(mock_client, Get(http://api.example.com/data)) .WillOnce(::testing::Return(hello)); // 执行 bool result service.Process(); // 验证 EXPECT_TRUE(result); // Google Mock会自动验证所有期望是否满足 }通过依赖注入构造函数传入HttpClient*和接口抽象我们完全隔离了网络依赖测试变得快速、稳定、可预测。这是编写高质量单元测试的关键技巧。3. 死亡测试Death Test用于测试程序是否在预期的情况下崩溃例如断言失败、内存访问违规。gtest提供了ASSERT_DEATH等宏。TEST(InvalidInputDeathTest, DiesOnNullPointer) { int* ptr nullptr; // 期望下一行代码会导致SIGSEGV崩溃在支持的系统上 ASSERT_DEATH(*ptr 5, .*); }死亡测试通常在一个独立的子进程中运行以避免主测试进程崩溃。使用时要小心并确保理解其行为。4. 测试覆盖率知道测试覆盖了多少代码很重要。可以使用像gcovGCC、llvm-covClang或Visual Studio的内置工具来生成覆盖率报告。在CMake中可以添加编译标志--coverageGCC/Clang对应-fprofile-arcs -ftest-coverage并链接lgcov。运行测试后使用工具生成HTML报告。高覆盖率不能保证没bug但低覆盖率一定意味着有很多代码路径没被测试过。我个人在实际项目中的体会是gflags和gtest的引入会显著提升项目的工程化水平。初期可能会觉得写测试麻烦但当你需要重构一个核心模块或者排查一个线上问题时一套完备的测试和清晰的配置管理就是最可靠的“安全网”和“导航图”。花时间搭建好这个基础框架在项目的整个生命周期里都会持续带来回报。最后一个小技巧可以把常用的gtest命令行参数如--gtest_coloryes彩色输出--gtest_repeat100重复测试以发现偶发bug写在一个脚本里或者通过环境变量GTEST_OPTIONS来设置让测试运行更高效。
返回列表