返回专辑
·Johan·4 分钟阅读

单测确定性:随机、时间与文件系统打桩

单测确定性:固定 seed、mock 时钟、避免 flaky 并发测试。

单测确定性:随机、时间与文件系统打桩

1. CI 绿、merge 后 flake

融合单测本地十遍过,CI 并行跑偶尔红——差在 sleep(10ms) 赌 watchdog、random_device 非确定、两个 TEST 写同一 /tmp/state。pose 差 1e-6 就要查 seed 和 clock;flake 等于没有 test,团队很快学会红了重跑。确定性不是洁癖,是可维护测试库的前提——机器人 regression 靠 golden,golden 靠 pin seed、clock、路径与浮点容差。flaky test 的隐性成本是:真人开始忽略 CI 红,真 bug 混在「再跑一遍」里溜进 main。把「CI 红了重跑」写进团队习惯,等于主动放弃 regression 网——确定性测试是一次性投资,回报是每次 merge 的可信度。

2. 固定 RNG 与 golden

mt19937 gen(12345) 写进 TEST 开头;CI 别 random_device 当 seed。RANSAC golden:固定 inlier seed、max iterations。矩阵 isApprox,标量 EXPECT_NEAR,容差写进 TEST 并注明来源。std::shuffle 测试用固定 seed。失败时 log 打印 seed 便于本地复现——CI artifact 里应能看到。golden 文件变更必须 PR 说明「为何数值变」——是算法改进还是容差太紧/seed 漂移,reviewer 应能判断。同一 TEST 内多处用 RNG 时共享同一 gen 实例,别每个 helper 各自 random_device——否则局部确定性也保不住。

cpp
TEST(Fusion, ImuLidarAlign) {
  std::mt19937 gen(12345);
  auto cloud = synthesizeCloud(gen);
  auto pose = fuse(cloud, fixedImuStream());
  EXPECT_TRUE(pose.isApprox(expectedPose, 1e-6));
}

3. 注入 Clock,零 wall sleep

FakeClock advance(10ms) 触发 watchdog,比 sleep 快且确定。timeout、重试退避用 IClock::now(),测试注入 fake。可包 rclcpp::Clock test double 验证 timer 回调次数。任何「等一会儿也许就绪」的测法都是 flake 种子——sensor fusion 单测尤其忌 wall sleep,CI 并行时调度 jitter 放大。生产代码里禁止直接 chrono::steady_clock::now() 散落在逻辑深处——集中经 Clock 接口,测试才能一键 fake。

cpp
class FakeClock {
  std::chrono::nanoseconds t_{};
public:
  auto now() const { return t_; }
  void advance(std::chrono::milliseconds d) { t_ += d; }
};

4. 文件系统与并行 ctest

temp 目录必须 test_+uuid 唯一;tearDown remove_all。别写 /tmp/fixed 或仓库内固定路径——并行 ctest 互相踩。setenv TZ/LC_ALL 后 restore,否则污染后续 TEST。只读标定 yaml 可 fixture 共享;写入必须 per-test 目录。GoogleTest TempDir 或 mkdtemp,别假设 /tmp 全局可写且独占。失败 artifact 应打包 temp 目录内容,本地 --gtest_filter=SameTest 可对照 CI 落盘文件。

5. 共享 static 与 gtest 并行

TEST_F 共享 mutable static 会 order-dependent。隔离 fixture,或 -j1 抓依赖;根因是去掉 static 可变。GTEST_FLAG_SET(shuffle, false) 与固定 seed;--gtest-repeat=100 抓 race。失败 log 打印 current_test_info()->name() 与 seed 进 CI artifact。singleton 配置若在 TEST 间 mutable,并行必 flake——改为 per-fixture 实例或 const 全局只读。环境变量 TZ、LC_ALL、OMP_NUM_THREADS 若在 TEST 里修改而不 restore,后续 TEST 在 CI 顺序随机时会偶发失败——tearDown 里 restore 是确定性的一部分,不是可选卫生。

6. 浮点 golden 与平台差

不同 -mfpmath 或平台偶尔差最后一 bit——document 容差,别盲目 ==。Eigen isApprox eps 与数据量级匹配。CI 与本地同 compiler 跑 golden 对比;若 ARM/x86 双 CI,容差或 pin 编译 flags 写进文档,别 silent 放宽到测不动。大矩阵 golden 可只比对 norm 或 subset index,全元素 == 在跨平台 CI 里往往自找 flake。

7. 案例:Fusion 并行 flake

Fusion 测用 sleep 等 IMU 对齐,CI -j8 时 3% 失败。改成 FakeClock advance 后连跑 500 次全绿。根因是 wall time 与线程调度,不是算法——这类 fix 一次省团队无数「再跑一遍」。MR 若新增 sleep,reviewer 默认 reject 除非注释说明为何不能 fake clock,且 issue 跟踪后续 refactor。watchdog、重试退避、debounce 逻辑都应经 injectable clock 单测——直接测 advance 毫秒数比 sleep 快几个数量级,且与 CI 并行度无关,这是机器人 fusion 栈里最划算的确定性改造之一。

8. 验收

for i in 1..100; ctest -R Fusion 全绿。并行 ctest 无 temp 冲突;无 sleep 依赖 wall time。失败 artifact 含 test 名与 seed。新 TEST 禁止 random_device 与固定 /tmp 路径——review checklist 项。MR 若新增 sleep,reviewer 默认 reject 除非有注释说明为何不能 fake clock。CI job 可加 --gtest_repeat=50 夜间跑,flake 在 merge 前暴露。本地 pre-push 钩子跑 --gtest_shuffle 与 repeat 可提前抓 order-dependent,别等到 CI 红才修——成本是几十秒 CPU,收益是少一次 revert。

9. 与 CI、录包对齐

flake 若只出现在特定 runner 负载下,说明测的是调度而非逻辑——应 fake 时间轴。Deterministic test 是 regression 保险——投入一次打桩,省无数次再跑一遍也许过。录 bag 做 golden 时 pin 解码版本与 seed,否则「同 bag 不同结果」分不清是算法还是测试非确定。测试确定性是机器人栈长期可维护的底座,不是测试工程师的私人偏好。MR template 可加一行:「本 PR 新增/修改的 TEST 是否依赖 wall time、random_device、固定 /tmp?」——reviewer 勾选否才 approve,比事后 hunt flake 便宜。

← 全部文章

johan's blog