就在我认为我们为 PICO Radar 项目构建的基石已经足够坚固时,一个深刻的洞察让我停下了脚步,并开启了一段意料之外但收获颇丰的重构之旅。
今天,我想和大家分享的,不仅仅是一次技术升级,更是一次关于“如何正确地测试”的哲学思辨。我们告别了那些曾经服务于我们的、但日益显得笨拙的 Shell 脚本,并拥抱了一个由 C++ 驱动的、真正优雅的自动化测试世界。
梦开始的地方:Shell 脚本的“原罪”
在项目初期,为了快速验证我们的 C/S(客户端/服务器)架构,我编写了一系列 .sh
脚本。它们兢兢业业地完成了任务:启动服务器,运行一个或多个模拟客户端,然后通过检查日志和退出码来判断对错。
这种方式简单直接,但随着项目的成长,它的“原罪”也愈发暴露:
- 脆弱性:测试的成败高度依赖于
sleep
命令给与的“魔法延迟”。网络稍有波动,或者机器性能稍有不同,测试就可能因为时序问题而莫名失败。 - 平台枷锁:Shell 脚本让我们被焊死在了类 Unix 系统上。对于一个旨在跨平台的 C++ 项目而言,这无异于自断一臂。
- 调试黑洞:当一个脚本失败时,我们能得到的只是一行红色的
FAILURE
。我们无法单步调试,无法窥探服务器和客户端在交互瞬间的内部状态。这就像是在隔着厚厚的墙壁听诊,充满了猜测。 - 维护噩梦:测试逻辑分散在 C++ 代码和 Shell 脚本之间,任何一方的微小改动都可能让另一方崩溃。
我意识到,我们需要的不是一个“能用”的测试方案,而是一个能与我们项目一同成长、能给予我们信心的工程化测试系统。
顿悟:我们需要的不是“模拟”,而是“驱动”
真正的革命性想法来自于一个简单的提问:“为什么我们要像一个外部用户一样,通过命令行去‘运行’我们的软件来进行测试?为什么我们不能像一个开发者一样,直接在代码层面去‘驱动’它?”
我们已经拥有了强大的 GoogleTest 框架,我们为什么不利用它来直接调用我们的服务器和客户端逻辑呢?
这催生了我们的新目标:实现完全在 C++ 中运行的、进程内 (In-Process) 的集成测试。
重构之路:三步奠定优雅的基石
为了实现这个目标,我们需要对现有代码进行一次“小手术”,让它们从独立的“程序”变成可被调用的“模块”。
第一步:解放服务器
我们的 WebsocketServer
最初被设计为在 main
函数中一次性启动并运行到程序结束。为了能在测试中控制它,我进行了解耦:
run()
->start()
:将阻塞的run
方法,改造成了非阻塞的start
方法。它会完成所有的初始化(启动IO线程池、监听端口等),然后立即返回。- 完善
stop()
:实现了一个健壮的stop
方法。它会优雅地关闭所有连接,停止所有线程,并确保所有资源都被干净利落地回收。
现在,WebsocketServer
变成了一个生命周期可控的对象,我们可以在测试代码中像这样操作它:
// 在测试开始时
auto server = std::make_shared<WebsocketServer>(...);
server->start(HOST, PORT, /*threads=*/2);
// ... 执行测试逻辑 ...
// 在测试结束时,TearDown 会自动调用
server->stop();
第二步:解放客户端
同样地,我们的 mock_client
原本也是一个独立的程序。我将其一分为二:
mock_client_lib
:一个全新的静态库,封装了核心的SyncClient
类的所有逻辑。- 轻量级
main.cpp
:一个极简的入口点,它的唯一职责就是解析命令行参数,然后调用mock_client_lib
中的功能。
通过这个改造,SyncClient
变成了一个可以被 #include
和直接调用的“积木”,我们的测试代码终于可以直接与它对话。
第三步:编写真正的 C++ 集成测试
当所有的准备工作就绪后,奇迹发生了。我们之前的 run_auth_success_test.sh
脚本有20多行,充满了 sleep
和 kill
。而现在,新的 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 的根基,比以往任何时候都更加坚固。