C++单元测试实战:GTest环境搭建、核心概念与高级特性详解

📅 2026/7/22 4:55:53 👁️ 阅读次数 📝 编程学习
C++单元测试实战:GTest环境搭建、核心概念与高级特性详解

1. 项目概述:为什么我们需要GTest?

在C++项目的开发周期里,最让人头疼的往往不是实现一个复杂的功能,而是在你信心满满地提交代码后,测试同事或者CI流水线给你甩过来一连串的红色失败标记。更糟的是,有时候你只是修改了一个看似无关的模块,却引发了另一个遥远模块的崩溃。这种“牵一发而动全身”的困境,根源就在于代码模块之间错综复杂的依赖关系没有被清晰地隔离和验证。单元测试,就是解决这个问题的“外科手术刀”。

单元测试的核心思想,是把程序分解成最小可测试的单元(在C++中通常是类或函数),然后针对每一个单元,在隔离的环境中验证其行为是否符合预期。这听起来简单,但手工写测试代码繁琐且容易出错。你需要搭建测试环境、模拟各种输入、捕获输出、判断结果,最后还要清理现场。Google Test(简称GTest)的出现,就是把这一套流程标准化、自动化,让我们能像写普通代码一样,优雅地编写和组织测试用例。

我经历过从完全手写测试main函数,到使用简陋的测试宏,再到全面拥抱GTest的过程。可以说,GTest不仅仅是一个测试框架,它更是一种促使你写出更健壮、更模块化代码的工程实践。它提供的断言机制、测试夹具、死亡测试、参数化测试等特性,能覆盖从简单函数到复杂类、从正常流程到异常崩溃的绝大部分测试场景。当你为一个核心算法类写完一整套GTest用例后,后续的任何重构或优化,你都能在几分钟内通过运行测试来确认没有引入回归错误,这种安全感是无可替代的。

2. GTest环境搭建与项目集成

2.1 获取与编译GTest

GTest的获取方式随着时间在演变。早期我们习惯直接下载源码包,现在更推荐使用包管理工具或直接从Git仓库获取,这能更好地与现代构建系统集成。

方法一:使用包管理器(推荐)在Linux(如Ubuntu)上,可以直接使用apt安装开发包:

sudo apt-get install libgtest-dev

安装后,头文件通常在/usr/include/gtest,但库文件可能需要手动编译。你可以进入/usr/src/gtest目录,使用CMake进行编译:

cd /usr/src/gtest sudo cmake CMakeLists.txt sudo make # 将编译出的库文件(如libgtest.a, libgtest_main.a)拷贝到系统库目录,例如 /usr/lib sudo cp lib/*.a /usr/lib

这种方法的好处是系统级集成,简单。缺点是版本可能不是最新的。

方法二:源码集成(灵活性强)对于需要特定版本或希望将GTest作为项目子模块(submodule)的项目,从GitHub克隆是更好的选择。

git clone https://github.com/google/googletest.git cd googletest mkdir build && cd build cmake .. make

编译后,在build/lib目录下会生成libgtest.alibgtest_main.a等静态库。libgtest_main.a包含了main函数,如果你不想自己写main函数,链接这个库即可。

注意:我强烈建议将GTest的源码作为项目的一部分(例如放在third_party/googletest目录下),然后在项目的CMakeLists.txt中使用add_subdirectory引入。这样做的好处是所有开发者环境统一,CI/CD流水线也无需预装GTest,真正做到开箱即用。具体做法是在你的CMakeLists.txt中添加:

add_subdirectory(third_party/googletest) include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) # 然后你的测试可执行文件可以这样链接: target_link_libraries(your_test_target gtest gtest_main)

2.2 与构建系统集成(以CMake为例)

现代C++项目几乎都使用CMake作为构建系统,与GTest的集成非常优雅。下面是一个最简化的项目结构示例:

my_project/ ├── CMakeLists.txt ├── include/ │ └── calculator.h ├── src/ │ ├── calculator.cpp │ └── CMakeLists.txt └── tests/ ├── test_calculator.cpp └── CMakeLists.txt

CMakeLists.txt负责全局配置和引入子目录。tests/CMakeLists.txt则是专门为测试编写的:

# tests/CMakeLists.txt # 启用测试功能 enable_testing() # 查找GTest包。如果GTest是作为子模块引入的,这步可能不需要,直接用target_link_libraries即可。 find_package(GTest REQUIRED) # 添加你的测试可执行文件 add_executable(run_all_tests test_calculator.cpp) # 链接GTest库和你的主项目库 target_link_libraries(run_all_tests GTest::gtest GTest::gtest_main my_project_lib) # 将可执行文件注册为一个测试 add_test(NAME AllCalculatorTests COMMAND run_all_tests)

这样配置后,在构建目录下,不仅可以用./run_all_tests运行测试,还可以使用ctest命令来运行和管理所有测试,ctest会输出更简洁的汇总报告,并且支持并行测试、测试重试等高级功能。

2.3 在IDE中运行测试(VSCode示例)

如果你使用Visual Studio Code进行开发,配置好CMake Tools插件后,运行GTest测试会非常方便。确保你的launch.jsontasks.json配置正确,可以让VSCode直接编译并运行测试目标,并在“测试”视图中直观地看到通过/失败的用例。关键在于配置测试目标为可调试的程序,并设置正确的program路径和args(例如--gtest_color=yes来启用彩色输出)。

3. GTest核心概念与断言系统详解

3.1 TEST与TEST_F宏:测试用例的基石

GTest中最基本的两个宏是TEST()TEST_F()

  • TEST(TestSuiteName, TestName):用于测试不依赖于复杂环境或共享数据的独立函数。例如测试一个工具函数:
TEST(StringUtilsTest, ReverseString) { EXPECT_EQ(ReverseString("hello"), "olleh"); EXPECT_EQ(ReverseString(""), ""); EXPECT_EQ(ReverseString("a"), "a"); }

这里的TestSuiteNameStringUtilsTest)是测试套件名,用于逻辑分组相关的测试。TestNameReverseString)是具体的测试用例名。它们共同构成一个唯一的测试标识。

  • TEST_F(TestFixtureName, TestName):当多个测试用例需要相同的初始化和清理工作时,就需要使用测试夹具(Fixture)。TEST_F中的F就代表Fixture。你需要先定义一个继承自::testing::Test的夹具类,然后在其中设置SetUp()TearDown()方法(或者使用C++11的构造函数和析构函数)。
class DatabaseTest : public ::testing::Test { protected: void SetUp() override { // 在每个测试开始前执行,类似于构造函数 db.Connect("test.db"); db.ClearAllData(); } void TearDown() override { // 在每个测试结束后执行,类似于析构函数 db.Disconnect(); } Database db; }; TEST_F(DatabaseTest, InsertRecord) { EXPECT_TRUE(db.Insert("key1", "value1")); EXPECT_EQ(db.Query("key1"), "value1"); } TEST_F(DatabaseTest, QueryNonExistentKey) { EXPECT_THROW(db.Query("invalid_key"), std::out_of_range); }

关键点TEST_F中的每个测试用例运行在一个全新的夹具对象上。也就是说,DatabaseTest夹具会被实例化两次,两个测试中的db对象是独立的,一个测试对数据库的修改不会影响另一个。这保证了测试的隔离性。

3.2 断言(Assertions):测试的逻辑核心

断言是测试的灵魂,用于验证代码行为。GTest提供了丰富的断言宏,主要分两类:ASSERT_*EXPECT_*

  • ASSERT_*:致命断言。如果失败,当前测试用例会立即终止,GTest会跳出这个测试函数,继续运行下一个测试用例。适用于“没有这个条件,后续测试毫无意义”的场景,比如指针为空。
  • EXPECT_*:非致命断言。如果失败,GTest会记录错误,但继续执行当前测试函数中的后续语句。这能让你在一次测试运行中收集到所有失败点。

常用断言一览表:

断言类型宏示例检查条件
布尔条件EXPECT_TRUE(condition)条件为真
EXPECT_FALSE(condition)条件为假
值相等EXPECT_EQ(val1, val2)val1 == val2
值不等EXPECT_NE(val1, val2)val1 != val2
小于/大于EXPECT_LT(val1, val2),EXPECT_GT(val1, val2)val1 < val2,val1 > val2
字符串相等EXPECT_STREQ(str1, str2)C字符串相等 (strcmp)
EXPECT_STRNE(str1, str2)C字符串不等
EXPECT_STRCASEEQ(str1, str2)忽略大小写相等
浮点数比较EXPECT_FLOAT_EQ(val1, val2)近似相等(默认4ULPs误差)
EXPECT_DOUBLE_EQ(val1, val2)近似相等
EXPECT_NEAR(val1, val2, abs_error)差值在误差范围内
异常检查EXPECT_THROW(statement, exception_type)语句抛出特定异常
EXPECT_ANY_THROW(statement)语句抛出任何异常
EXPECT_NO_THROW(statement)语句不抛异常

关于浮点数比较的坑:永远不要用EXPECT_EQ比较浮点数!因为浮点数在计算机中的表示存在精度误差。EXPECT_FLOAT_EQEXPECT_DOUBLE_EQ使用基于ULPs(Units in the Last Place)的快速比较,在大多数情况下够用。如果需要更直观的绝对误差或相对误差控制,请使用EXPECT_NEAR

3.3 死亡测试(Death Tests):验证程序如何“优雅地崩溃”

有些函数在接收到非法输入时,预期行为不是返回错误码,而是直接崩溃(如调用abort()exit()或触发断言失败)。测试这种行为就是“死亡测试”。GTest提供了EXPECT_DEATH等宏来捕获并验证这种预期的死亡。

// 一个遇到除零错误就退出的危险函数 void DangerousDivide(int a, int b) { if (b == 0) { std::cerr << "Fatal: Division by zero!" << std::endl; std::abort(); } // ... 正常除法 } TEST(DangerousFuncTest, DeathOnDivideByZero) { // 验证当b=0时,语句会以“死亡”告终 EXPECT_DEATH(DangerousDivide(5, 0), "Fatal: Division by zero!"); // 第二个参数是正则表达式,匹配死亡前的标准错误输出。 }

重要提示:死亡测试在子进程中运行。这意味着在死亡测试语句中修改的全局变量或静态变量,在父进程(即测试主体)中是不可见的。同时,死亡测试的运行开销相对较大。

4. 高级特性:参数化、类型化与模拟

4.1 参数化测试(TEST_P):用数据驱动测试

当你需要用多组不同的输入数据来测试同一个逻辑时,写多个TEST用例显得冗余。参数化测试可以优雅地解决这个问题。 步骤分为三步:

  1. 创建一个继承自::testing::TestWithParam<T>的夹具类,其中T是参数类型。
  2. 使用TEST_P宏定义测试。
  3. 使用INSTANTIATE_TEST_SUITE_P宏实例化测试套件,并传入参数集合。
// 1. 定义参数化夹具。参数类型是 std::tuple<int, int, int> (被除数,除数,预期余数) class ModTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {}; // 2. 定义参数化测试 TEST_P(ModTest, ComputesRemainderCorrectly) { int dividend = std::get<0>(GetParam()); int divisor = std::get<1>(GetParam()); int expected_remainder = std::get<2>(GetParam()); EXPECT_EQ(Mod(dividend, divisor), expected_remainder); } // 3. 实例化测试套件,提供多组参数 INSTANTIATE_TEST_SUITE_P( ValidInputs, // 实例名称,会出现在测试输出中 ModTest, ::testing::Values( std::make_tuple(10, 3, 1), std::make_tuple(15, 5, 0), std::make_tuple(-10, 3, -1), // 测试负数 std::make_tuple(0, 7, 0) ));

运行后,GTest会为每一组参数生成一个独立的测试用例,并以ValidInputs/ModTest.ComputesRemainderCorrectly/0这样的格式命名,非常清晰。

4.2 类型化测试(TYPED_TEST):测试模板

如果你的代码是模板,需要对多种类型进行相同的测试,类型化测试非常有用。它和参数化测试类似,但参数是类型而非值。

// 1. 定义类型列表 typedef ::testing::Types<int, float, double> MyTypes; // 2. 创建类型化测试夹具(依然是模板类) template <typename T> class ContainerTest : public ::testing::Test {}; TYPED_TEST_SUITE(ContainerTest, MyTypes); // 关联夹具和类型列表 // 3. 定义类型化测试 TYPED_TEST(ContainerTest, IsEmptyAfterCreation) { TypeParam container; // 使用 TypeParam 获取当前测试的类型 EXPECT_TRUE(container.empty()); }

GTest会为MyTypes中的每一种类型(int,float,double)实例化并运行ContainerTest测试套件中的所有测试。

4.3 模拟(Mocking)与GoogleMock简介

单元测试强调“隔离”,但被测对象往往依赖其他复杂的模块(如数据库、网络服务)。这时,我们需要用“模拟对象”(Mock Object)来替代真实的依赖。模拟对象允许你预设这些依赖的行为(比如“当调用A方法时,返回B值”)和检查交互(比如“验证C方法被调用了恰好一次”)。

GTest通常与GoogleMock(GMock)配合使用,GMock是一个功能强大的模拟框架。它的使用模式通常是:

  1. 定义一个模拟类接口。
  2. 在测试中创建模拟对象,并设置期望(Expectations)。
  3. 将被测对象与模拟对象连接,运行测试。
  4. GMock在测试结束时自动验证所有期望是否满足。
// 假设我们有一个依赖“文件读取器”的类 class FileParser { public: FileParser(FileReader* reader) : reader_(reader) {} std::string ParseFirstLine() { // 它依赖reader_来读取内容 std::string content = reader_->ReadAll(); // ... 解析逻辑 return first_line; } private: FileReader* reader_; }; // 使用GMock测试FileParser,而不需要真实的文件系统 class MockFileReader : public FileReader { public: MOCK_METHOD(std::string, ReadAll, (), (override)); }; TEST(FileParserTest, ParsesFirstLine) { MockFileReader mock_reader; // 设置期望:当ReadAll()被调用时,返回一个预设的字符串 EXPECT_CALL(mock_reader, ReadAll()) .WillOnce(::testing::Return("First line\nSecond line\n")); FileParser parser(&mock_reader); EXPECT_EQ(parser.ParseFirstLine(), "First line"); // 测试结束时,GMock会自动验证ReadAll()被调用了一次。 }

模拟是进行真正单元测试的关键,它让你能专注于被测单元自身的逻辑,而无需担心外部系统的不稳定或复杂性。

5. 测试实战:一个完整案例解析

让我们为一个简单的Calculator类编写完整的GTest用例。这个类有加、减、乘、除和累加功能。

calculator.h:

#pragma once #include <vector> class Calculator { public: int Add(int a, int b); int Subtract(int a, int b); int Multiply(int a, int b); double Divide(int a, int b); // 返回double,可能除不尽 int Accumulate(const std::vector<int>& numbers); };

test_calculator.cpp:

#include "calculator.h" #include <gtest/gtest.h> #include <stdexcept> // 1. 基础功能测试 (使用 TEST) TEST(CalculatorTest, AddReturnsSumOfTwoIntegers) { Calculator calc; EXPECT_EQ(calc.Add(10, 20), 30); EXPECT_EQ(calc.Add(-5, 10), 5); EXPECT_EQ(calc.Add(0, 0), 0); } TEST(CalculatorTest, SubtractReturnsDifference) { Calculator calc; EXPECT_EQ(calc.Subtract(20, 10), 10); EXPECT_EQ(calc.Subtract(10, 20), -10); } // 2. 测试浮点数除法 (注意使用 EXPECT_NEAR) TEST(CalculatorTest, DivideReturnsCorrectResult) { Calculator calc; EXPECT_DOUBLE_EQ(calc.Divide(10, 2), 5.0); EXPECT_NEAR(calc.Divide(10, 3), 3.333333, 1e-6); // 使用绝对误差 } // 3. 测试异常行为(假设除数为0时我们抛异常) TEST(CalculatorTest, DivideByZeroThrowsException) { Calculator calc; EXPECT_THROW(calc.Divide(10, 0), std::invalid_argument); } // 4. 使用测试夹具 (TEST_F) 测试累加功能 class CalculatorAccumulateTest : public ::testing::Test { protected: void SetUp() override { // 公共的测试数据准备 positive_numbers_ = {1, 2, 3, 4, 5}; empty_vector_ = {}; single_element_ = {42}; } Calculator calc_; std::vector<int> positive_numbers_; std::vector<int> empty_vector_; std::vector<int> single_element_; }; TEST_F(CalculatorAccumulateTest, SumsVectorOfPositiveNumbers) { EXPECT_EQ(calc_.Accumulate(positive_numbers_), 15); } TEST_F(CalculatorAccumulateTest, ReturnsZeroForEmptyVector) { EXPECT_EQ(calc_.Accumulate(empty_vector_), 0); } TEST_F(CalculatorAccumulateTest, HandlesSingleElement) { EXPECT_EQ(calc_.Accumulate(single_element_), 42); } // 5. 参数化测试:测试乘法交换律 class MultiplyCommutativeTest : public ::testing::TestWithParam<std::tuple<int, int>> {}; TEST_P(MultiplyCommutativeTest, HoldsCommutativeProperty) { int a = std::get<0>(GetParam()); int b = std::get<1>(GetParam()); Calculator calc; EXPECT_EQ(calc.Multiply(a, b), calc.Multiply(b, a)); } INSTANTIATE_TEST_SUITE_P( VariousIntegers, MultiplyCommutativeTest, ::testing::Combine( ::testing::Values(-5, 0, 1, 10, 100), // a 的值 ::testing::Values(-3, 0, 2, 7, 50) // b 的值 ));

编写测试的心得

  • 测试用例命名要清晰AddReturnsSumOfTwoIntegersTestAdd好得多。清晰的命名在测试失败时能让你立刻知道是哪个功能出了问题。
  • 一个测试只验证一件事AddReturnsSumOfTwoIntegers只测试加法功能。不要在一个测试里又测加法又测边界条件。这符合单元测试的“单一职责”原则。
  • 使用夹具组织共享设置CalculatorAccumulateTest夹具为所有累加测试提供了统一的Calculator实例和测试数据,避免了重复代码。
  • 参数化测试覆盖边界和一般情况MultiplyCommutativeTest用多组数据验证了乘法的交换律,包括正数、负数、零。

6. 测试组织、运行与调试技巧

6.1 测试发现与过滤

当项目有成百上千个测试时,你不可能每次都运行全部。GTest提供了强大的命令行参数来过滤测试。

  • 运行所有测试./your_test_executable
  • 运行特定测试套件./your_test_executable --gtest_filter=CalculatorTest.*
  • 运行名称包含特定字符串的测试./your_test_executable --gtest_filter=*Death*
  • 排除某些测试./your_test_executable --gtest_filter=-*Accumulate*(注意-号)
  • 列出所有测试而不运行./your_test_executable --gtest_list_tests

在CMake+CTest的上下文中,你可以在构建目录下使用ctest -R <regex>来运行匹配正则表达式的测试。

6.2 测试输出与XML报告

默认情况下,GTest的输出是彩色的,易于阅读。但你也可以生成机器可读的XML报告,便于CI系统(如Jenkins, GitLab CI)解析和展示。

./your_test_executable --gtest_output=xml:report.xml

生成的report.xml包含了每个测试用例的执行时间、状态(通过/失败)、失败信息等。许多CI工具都有插件可以直接可视化这种JUnit风格的报告。

6.3 调试失败的测试

当测试失败时,GTest会打印出详细的失败信息,包括哪个文件哪一行、期望值是什么、实际值是什么。但有时这还不够,你需要调试。

  1. 使用IDE调试器:这是最直接的方法。在VSCode或CLion中,将测试可执行文件设置为调试目标,在失败的EXPECT_EQ处设置断点。
  2. 使用--gtest_break_on_failure:这个命令行参数会在第一个断言失败时触发一个断点(在类Unix系统上调用raise(SIGTRAP)),方便你立刻进入调试器查看现场。
  3. 输出更多信息:在测试中使用std::coutSCOPED_TRACE宏输出中间变量值。SCOPED_TRACE的好处是,它的消息会出现在GTest的失败信息中。
    TEST(ComplexTest, SomeOperation) { int intermediate_result = DoComplexStep1(); SCOPED_TRACE("After Step 1, intermediate_result=" + std::to_string(intermediate_result)); EXPECT_EQ(DoComplexStep2(intermediate_result), kExpectedValue); }

6.4 测试夹具的SetUp/TearDown与构造函数/析构函数

TEST_F中,初始化工作可以在夹具类的构造函数中完成,也可以在SetUp()方法中完成。它们有细微差别:

  • 构造函数/析构函数:是C++对象的固有机制。如果初始化不依赖于GTest框架本身,放在构造函数里更自然。
  • SetUp()/TearDown():是GTest框架提供的钩子。GTest官方文档建议使用它们,因为框架可能会在调用SetUp()之前做一些自己的初始化工作(虽然大部分情况下没区别)。更重要的是,如果初始化失败并抛出异常,在SetUp()中抛出可以被GTest框架捕获并标记测试为失败,而在构造函数中抛出可能会导致程序异常终止。

我的经验是:对于简单的资源获取(如分配内存、打开临时文件),放在构造函数/析构函数里没问题。如果初始化动作本身就是测试的一部分,或者可能失败且你需要GTest处理这个失败,那么用SetUp()/TearDown()更稳妥。

7. 常见陷阱、最佳实践与进阶思考

7.1 测试中的常见陷阱

  1. 测试彼此依赖(有状态测试):这是最隐蔽的问题。例如,测试A修改了一个全局变量或静态变量,测试B的运行结果因此改变。解决方案:始终让测试保持独立。使用TEST_F为每个测试创建全新的夹具实例,避免使用全局变量或静态变量存储测试状态。
  2. “脆弱测试”(Brittle Tests):测试依赖于外部环境(如特定文件路径、网络状态、系统时间)或内部未公开的实现细节(如私有成员变量、函数调用顺序)。一旦环境变化或实现重构,测试就失败。解决方案:测试公共接口和行为,而不是私有实现。使用模拟(Mock)来隔离外部依赖。对于时间等依赖,可以抽象成接口并进行模拟。
  3. 测试过于复杂:测试代码本身比被测试代码还难懂。这说明要么被测单元太大(违反了“单一职责”),要么测试写得不好。解决方案:重构被测代码,使其更小、更专注。保持测试代码简单、直白。
  4. 忽略测试失败:CI上测试失败了,但大家因为“赶进度”而暂时忽略。这会让测试套件逐渐失去价值。解决方案:将测试失败视为最高优先级的Bug来处理。保持测试套件始终为绿色。

7.2 单元测试最佳实践

  • FIRST原则
    • Fast(快速):测试应该能快速运行,鼓励频繁执行。
    • Independent(独立):测试之间不应有依赖。
    • Repeatable(可重复):在任何环境中(开发机、CI服务器)结果都应一致。
    • Self-Validating(自验证):测试结果应该是布尔值(通过/失败),无需人工判断。
    • Timely(及时):最好在编写生产代码的同时或之前编写测试代码(测试驱动开发TDD)。
  • 测试命名规范TestSuiteName_ScenarioName_ExpectedBehavior是一个好模式,例如ParserTest_EmptyInput_ThrowsInvalidArgument
  • 测试代码质量:测试代码也是代码,需要保持清晰、可维护。适当抽取公共辅助函数,避免重复。
  • 覆盖率是指导,不是目标:追求100%的测试覆盖率通常不现实且性价比低。应该关注核心逻辑、复杂分支和边界条件的覆盖。使用像gcovllvm-cov这样的工具生成覆盖率报告,找出未被覆盖的代码块,并思考它们是否需要测试。

7.3 在大型项目中的测试策略

在大型C++项目中,测试的组织是一门艺术。

  • 分层测试:建立测试金字塔。单元测试(GTest)是底座,数量最多,运行最快。之上是集成测试(测试模块间交互),再上是端到端(E2E)系统测试。GTest主要服务于单元测试和部分集成测试。
  • 测试目标拆分:不要将所有测试编译进一个巨大的可执行文件。应该按模块或功能拆分测试目标。例如,为network模块、database模块、algorithm模块分别创建network_testsdatabase_testsalgorithm_tests。这样可以利用构建系统的并行性,加速编译和测试。
  • 与CI/CD流水线集成:将测试作为CI流水线的核心环节。通常的步骤是:代码推送 -> 触发CI -> 编译所有目标 -> 运行单元测试 -> 运行集成测试 -> 部署到测试环境。单元测试失败应该阻止后续步骤。
  • 处理测试数据:单元测试应避免依赖真实数据库或文件。使用内存数据库(如SQLite:memory:)或临时文件。对于复杂的测试数据,可以定义在SetUp()中,或者使用工厂函数创建。对于需要共享的只读测试数据(如大的参考文件),可以将其放在项目的test_data目录下,在测试中通过相对路径读取。

最后,记住单元测试的终极目的不是写测试,而是通过测试的反馈来驱动你写出更清晰、更模块化、更可靠的代码。当你养成了为每个新功能编写测试的习惯后,你会发现代码的设计质量会自然而然地提升,因为难以测试的代码,往往本身就是设计不良的代码。GTest就是这个过程中一个强大而可靠的伙伴。