测试的革命——从脆弱脚本到优雅的进程内测试 – #10
本文最后更新于 36 天前,其中的信息可能已经有所发展或是发生改变。

就在我认为我们为 PICO Radar 项目构建的基石已经足够坚固时,一个深刻的洞察让我停下了脚步,并开启了一段意料之外但收获颇丰的重构之旅。

今天,我想和大家分享的,不仅仅是一次技术升级,更是一次关于“如何正确地测试”的哲学思辨。我们告别了那些曾经服务于我们的、但日益显得笨拙的 Shell 脚本,并拥抱了一个由 C++ 驱动的、真正优雅的自动化测试世界。

梦开始的地方:Shell 脚本的“原罪”

在项目初期,为了快速验证我们的 C/S(客户端/服务器)架构,我编写了一系列 .sh 脚本。它们兢兢业业地完成了任务:启动服务器,运行一个或多个模拟客户端,然后通过检查日志和退出码来判断对错。

这种方式简单直接,但随着项目的成长,它的“原罪”也愈发暴露:

  • 脆弱性:测试的成败高度依赖于 sleep 命令给与的“魔法延迟”。网络稍有波动,或者机器性能稍有不同,测试就可能因为时序问题而莫名失败。
  • 平台枷锁:Shell 脚本让我们被焊死在了类 Unix 系统上。对于一个旨在跨平台的 C++ 项目而言,这无异于自断一臂。
  • 调试黑洞:当一个脚本失败时,我们能得到的只是一行红色的 FAILURE。我们无法单步调试,无法窥探服务器和客户端在交互瞬间的内部状态。这就像是在隔着厚厚的墙壁听诊,充满了猜测。
  • 维护噩梦:测试逻辑分散在 C++ 代码和 Shell 脚本之间,任何一方的微小改动都可能让另一方崩溃。

我意识到,我们需要的不是一个“能用”的测试方案,而是一个能与我们项目一同成长、能给予我们信心的工程化测试系统

顿悟:我们需要的不是“模拟”,而是“驱动”

真正的革命性想法来自于一个简单的提问:“为什么我们要像一个外部用户一样,通过命令行去‘运行’我们的软件来进行测试?为什么我们不能像一个开发者一样,直接在代码层面去‘驱动’它?”

我们已经拥有了强大的 GoogleTest 框架,我们为什么不利用它来直接调用我们的服务器和客户端逻辑呢?

这催生了我们的新目标:实现完全在 C++ 中运行的、进程内 (In-Process) 的集成测试。

重构之路:三步奠定优雅的基石

为了实现这个目标,我们需要对现有代码进行一次“小手术”,让它们从独立的“程序”变成可被调用的“模块”。

第一步:解放服务器

我们的 WebsocketServer 最初被设计为在 main 函数中一次性启动并运行到程序结束。为了能在测试中控制它,我进行了解耦:

  1. run() -> start():将阻塞的 run 方法,改造成了非阻塞的 start 方法。它会完成所有的初始化(启动IO线程池、监听端口等),然后立即返回。
  2. 完善 stop():实现了一个健壮的 stop 方法。它会优雅地关闭所有连接,停止所有线程,并确保所有资源都被干净利落地回收。

现在,WebsocketServer 变成了一个生命周期可控的对象,我们可以在测试代码中像这样操作它:

// 在测试开始时
auto server = std::make_shared<WebsocketServer>(...);
server->start(HOST, PORT, /*threads=*/2);

// ... 执行测试逻辑 ...

// 在测试结束时,TearDown 会自动调用
server->stop();

第二步:解放客户端

同样地,我们的 mock_client 原本也是一个独立的程序。我将其一分为二:

  1. mock_client_lib:一个全新的静态库,封装了核心的 SyncClient 类的所有逻辑。
  2. 轻量级 main.cpp:一个极简的入口点,它的唯一职责就是解析命令行参数,然后调用 mock_client_lib 中的功能。

通过这个改造,SyncClient 变成了一个可以被 #include 和直接调用的“积木”,我们的测试代码终于可以直接与它对话。

第三步:编写真正的 C++ 集成测试

当所有的准备工作就绪后,奇迹发生了。我们之前的 run_auth_success_test.sh 脚本有20多行,充满了 sleepkill。而现在,新的 C++ 测试用例是这样的:

// test/integration_tests/test_auth.cpp

TEST_F(AuthIntegrationTest, AuthenticationShouldSucceedWithCorrectToken) {
    // Arrange: 启动服务器
    server_->start(HOST, PORT, 1);

    // Act: 创建一个客户端并直接运行认证逻辑
    mock_client::SyncClient client;
    int result = client.run(HOST, std::to_string(PORT), CORRECT_TOKEN, "--test-auth-success", "player_good");

    // Assert: 我们可以直接验证返回值,甚至服务器的内部状态!
    EXPECT_EQ(result, 0);
    EXPECT_EQ(registry_->getPlayerCount(), 1); // <-- 这是旧方法无法想象的!
}

看,这就是优雅!

  • 没有 sleep:所有操作都在同一个进程内,由操作系统调度,时序精确无误。
  • 没有 Process:我们不再需要管理子进程,代码更简洁。
  • 闪电般的速度:启动一个 C++ 对象和启动一个操作系统进程,其间的速度差异是天壤之别。我们的整个测试套件现在几乎是瞬间完成。
  • 无与伦比的洞察力:我们不仅能检查客户端的返回值,还能直接断言服务器内部的状态(registry_->getPlayerCount()),这给予了我们前所未有的信心。
  • 完美的调试体验:我可以在 EXPECT_EQ 上设置一个断点,然后一路单步跟踪进入 client.run(),再跳进 server 的处理逻辑。整个调用链一览无余。

结论:一次思想的飞跃

我们用同样的方式,将认证失败、数据广播、服务发现等所有集成测试都迁移到了新的框架下。曾经脆弱、缓慢、不透明的测试流程,如今变得健壮、快速、晶莹剔透。

这次重构带给我的,远不止是技术上的改进。它更是一次思想上的飞跃——要像开发者一样去测试,而不是像用户一样去运行。通过将我们的软件模块化、接口化,我们不仅让它变得更易于测试,也从根本上提升了它的设计质量。

现在,每当我提交代码,CI 流水线上那些绿色的对勾,都带给了我前所未有的安心。我知道,PICO Radar 的根基,比以往任何时候都更加坚固。

本文作者:SakuraPuare
本文链接:https://blog.sakurapuare.com/archives/2025/07/picoradar-dev-10/
版权声明:本文采用 CC BY-NC-SA 4.0 CN 协议进行许可
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇