26、测试框架:Google Test跨平台使用、模拟(Mocking)、覆盖率分析

说实话,测试这件事,很多C++开发者一开始都不太重视。我早年带团队的时候,经常听到一句话:「代码我写得很稳,不用测。」结果呢?上线就崩,加班修bug,搞得大家都很痛苦。后来我强制团队上了Google Test,才真正体会到什么叫「睡得安稳」。

这一章,我们就来聊聊Google Test在跨平台场景下的实战用法。包括怎么写测试、怎么模拟外部依赖、怎么分析覆盖率。嗯,都是我在项目里踩过坑之后总结出来的经验。

26.1 Google Test的跨平台搭建

Google Test(简称gtest)是Google开源的C++测试框架。它最大的好处就是——跨平台。Windows、Linux、macOS,甚至嵌入式Linux都能跑。我曾在ARM板子上跑过gtest,效果还不错。

怎么引入呢?我建议用CMake的FetchContent,简单又干净:

# CMakeLists.txt
include(FetchContent)
FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/release-1.12.1.tar.gz
)
FetchContent_MakeAvailable(googletest)

enable_testing()
add_executable(my_test test_main.cpp)
target_link_libraries(my_test GTest::GTest GTest::Main)
add_test(NAME my_test COMMAND my_test)

这样写,不管你在哪个平台,CMake都会自动下载编译gtest。省心。

小提示: 如果你在公司内网,不能访问GitHub,可以先把源码下载到本地,用 add_subdirectory 引入。

26.2 编写第一个测试用例

写测试其实很简单。你只需要包含头文件,然后用宏定义测试用例:

#include <gtest/gtest.h>

int Add(int a, int b) {
    return a + b;
}

TEST(AddTest, PositiveNumbers) {
    EXPECT_EQ(Add(1, 2), 3);
    EXPECT_EQ(Add(10, 20), 30);
}

TEST(AddTest, NegativeNumbers) {
    EXPECT_EQ(Add(-1, -2), -3);
    EXPECT_EQ(Add(-5, 5), 0);
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

这里有两个关键点:

  • TEST 宏的第一个参数是测试套件名,第二个是测试名。可以自由组合。
  • EXPECT_EQ 是断言宏,如果失败,测试继续执行。还有 ASSERT_EQ,失败就终止当前测试。

我个人习惯用 EXPECT_* 系列,因为一个测试里可能有多个检查点,我不想因为第一个失败就跳过后面的。

26.3 模拟(Mocking)实战

真实项目中,你的代码往往依赖外部模块——比如数据库、网络、文件系统。直接测这些依赖,测试会变得很慢、不稳定。这时候就需要模拟(Mock)。

Google Test自带了一个Mock框架,叫Google Mock。用法也很直接。

假设你有一个网络接口:

class NetworkClient {
public:
    virtual ~NetworkClient() = default;
    virtual bool SendData(const std::string& data) = 0;
    virtual std::string ReceiveData() = 0;
};

你想测试一个业务类,它依赖这个网络客户端。那就可以写一个Mock类:

#include <gmock/gmock.h>

class MockNetworkClient : public NetworkClient {
public:
    MOCK_METHOD(bool, SendData, (const std::string& data), (override));
    MOCK_METHOD(std::string, ReceiveData, (), (override));
};

然后在测试里设置预期行为:

TEST(BusinessLogicTest, SendAndReceive) {
    MockNetworkClient mock_client;
    BusinessLogic logic(&mock_client);

    EXPECT_CALL(mock_client, SendData("hello"))
        .Times(1)
        .WillOnce(::testing::Return(true));

    EXPECT_CALL(mock_client, ReceiveData())
        .Times(1)
        .WillOnce(::testing::Return("world"));

    logic.Execute();
}
重点: EXPECT_CALL 不仅设置返回值,还检查调用次数。如果实际调用次数和预期不符,测试会失败。这能帮你发现「少调了」或「多调了」的bug。

我曾经在项目中遇到过一个问题:某个模块在异常路径下忘记调用清理函数,导致资源泄漏。用Mock一测,发现清理函数根本没被调用,测试直接报红。嗯,这种bug靠肉眼review很难发现,但Mock能精准抓到。

26.4 覆盖率分析

测试写完了,怎么知道测得全不全?覆盖率分析就是干这个的。

在Linux/macOS上,我常用 gcov + lcov 这套组合。配合CMake,配置起来也不复杂:

# CMakeLists.txt 中添加
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")

编译运行测试后,执行:

lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

然后打开 coverage_report/index.html,就能看到每个文件的覆盖率了。行覆盖率、分支覆盖率一目了然。

注意: 覆盖率不是越高越好。我见过有人为了100%覆盖率,写了很多「为了测而测」的用例,反而浪费了时间。我个人建议:核心逻辑覆盖到90%以上,边界条件和异常路径重点覆盖。

在Windows上,可以用OpenCppCoverage。用法类似,命令行跑一下就能生成报告。

26.5 跨平台测试的坑与避让

跨平台测试,最头疼的就是平台差异。我总结了几条经验:

  • 文件路径分隔符: Windows用反斜杠,Linux用正斜杠。测试里写路径时,最好用 /,或者用CMake的 file(TO_CMAKE_PATH) 转换。
  • 换行符差异: Windows是 \r\n,Linux是 \n。比较文件内容时,记得统一处理。
  • 浮点数精度: 不同平台浮点运算结果可能有微小差异。用 EXPECT_FLOAT_EQEXPECT_NEAR 代替 EXPECT_EQ
  • 线程调度: 多线程测试在Windows和Linux上的行为可能不同。建议用 Sleepstd::this_thread::sleep_for 做适当的等待,而不是依赖时序。

我记得有一次,测试在Linux上全绿,到了Windows上就挂。查了半天,发现是某个临时文件路径写死了 /tmp/。Windows上根本没有这个目录。从那以后,我所有测试都改用 std::filesystem::temp_directory_path()

26.6 知识体系总览

下面这张图,是我对本章知识点的梳理。你可以把它当作一个快速索引:

Google Test 跨平台测试 跨平台搭建 CMake FetchContent Windows / Linux / macOS 嵌入式平台支持 测试用例编写 TEST / TEST_F 宏 EXPECT_* vs ASSERT_* 测试夹具 (SetUp/TearDown) 模拟 (Mocking) MOCK_METHOD 宏 EXPECT_CALL 设置 WillOnce / WillRepeatedly 覆盖率分析 gcov + lcov (Linux) OpenCppCoverage (Win) 行/分支覆盖率报告 跨平台避坑指南 路径分隔符 / 换行符 浮点数精度差异 线程调度行为不同

26.7 写在最后

测试框架这东西,用起来不难,难的是坚持。我见过很多项目,一开始热情高涨写测试,后来赶进度就放弃了。结果呢?bug越积越多,最后连重构都不敢动。

我个人建议:从今天开始,每个新功能都配上测试。哪怕只是一个简单的 TEST,也比没有强。等你习惯了,你会发现——写测试其实比写代码更让人安心。

好了,这一章就到这里。记住:测试不是负担,是你的护身符。


公众号:蓝海资料掘金营,微信 deep3321