公司动态

第327篇 单元测试实战——Google Test在机器人项目中的应用

📅 2026/9/2 15:29:38
第327篇 单元测试实战——Google Test在机器人项目中的应用
上篇聊了端侧AI部署的工程实践。模型部署到嵌入式设备后你怎么保证推理代码的正确性换个模型版本输出还一致吗精度量化后数值偏差在可接受范围内吗这些问题的答案都指向同一个东西测试。很多做机器人的工程师写代码靠跑一下看看来验证。在仿真里跑一遍轨迹没问题就算过了。这种做法在小项目里凑合能用一旦代码量上去了、模块多起来了bug藏在哪你根本不知道。今天聊聊单元测试重点讲Google Test在机器人项目中的实战应用。Google Test基础五分钟上手Google Test简称GTest是C领域最主流的测试框架几乎是行业标准。ROS2的很多包内部就用的GTest。#include gtest/gtest.h int add(int a, int b) { return a b; } TEST(MathTest, AddPositive) { EXPECT_EQ(add(2, 3), 5); } TEST(MathTest, AddNegative) { EXPECT_EQ(add(-1, -2), -3); }EXPECT_EQ和ASSERT_EQ是最常用的两个断言宏。区别在于EXPECT_EQ失败后继续执行后面的断言ASSERT_EQ失败后直接退出当前测试用例。一般用EXPECT_EQ除非后面的断言依赖前面必须成立的条件。CMakeLists.txt里集成GTest也很简单find_package(GTest REQUIRED) add_executable(math_test test_math.cpp) target_link_libraries(math_test GTest::gtest_main) gtest_discover_tests(math_test)加上gtest_discover_tests之后ctest命令就能自动发现并运行所有测试用例。CI里跑测试就一条命令的事。机器人项目的测试策略机器人代码和普通业务代码有个本质区别它和硬件、时间、物理世界强耦合。电机转速依赖硬件反馈SLAM依赖传感器数据流控制循环依赖实时时钟。这些东西怎么做单元测试核心思路是隔离。把算法逻辑和硬件依赖剥离开只测纯算法部分。举个例子你写了个PID控制器。PID的输入是目标值和当前反馈值输出是控制量。这个计算过程是纯数学运算不依赖任何硬件。直接构造输入、调用函数、检查输出就是一个标准的单元测试。TEST(PIDTest, ProportionalOnly) { PIDController pid(1.0, 0.0, 0.0); double output pid.compute(10.0, 0.0, 0.01); EXPECT_NEAR(output, 10.0, 1e-6); } TEST(PIDTest, IntegralAccumulation) { PIDController pid(0.0, 1.0, 0.0); pid.compute(5.0, 0.0, 0.1); double output pid.compute(5.0, 0.0, 0.1); EXPECT_NEAR(output, 10.0, 1e-6); }对于依赖硬件的模块用Mock对象来替代。比如你的里程计模块需要读取编码器数据测试时用Mock编码器返回预设值验证里程计的计算逻辑是否正确。Google MockGMock是GTest配套的mock框架用起来很方便。class MockEncoder : public EncoderInterface { public: MOCK_METHOD(double, getVelocity, (), (override)); MOCK_METHOD(int64_t, getTicks, (), (override)); }; TEST(OdometryTest, StraightLineMotion) { auto mock_enc std::make_sharedMockEncoder(); EXPECT_CALL(*mock_enc, getVelocity()) .WillRepeatedly(Return(1.0)); Odometry odom(mock_enc, 0.1); // wheel_radius0.1 odom.update(0.1); // dt0.1s EXPECT_NEAR(odom.getX(), 0.1, 1e-6); }数值计算的测试技巧机器人领域大量涉及浮点运算测试数值计算有个特殊问题精度。两个理论上应该相等的浮点数实际计算结果可能差个1e-10。用EXPECT_EQ直接比较肯定挂。EXPECT_NEAR是解决方案——它允许你指定一个误差范围。但这个误差范围tolerance怎么定定太大测不出问题定太小频繁误报。经验法则是根据算法的数值稳定性来定。矩阵求逆、三角函数这类操作tolerance一般设1e-6到1e-8。涉及大量累加的操作比如积分tolerance要放宽到1e-4。如果不确定先跑一遍参考实现看实际误差分布在哪里。还有个技巧是相对误差。当数值本身很大的时候绝对误差可能很大但相对误差很小。比如两个值都是10000量级差0.001绝对误差0.001看起来不小相对误差才1e-7。这种情况下用相对误差判断更合理。double expected 10000.0; double actual 10000.001; EXPECT_NEAR(actual / expected, 1.0, 1e-6);参数化测试在数值计算中特别好用。你有一组输入输出数据不想为每组写一个TEST用TEST_P批量跑class KinematicsTest : public ::testing::TestWithParam std::tupledouble, double, double {}; TEST_P(KinematicsTest, ForwardKinematics) { auto [j1, j2, expected_x] GetParam(); double x forward_kin(j1, j2); EXPECT_NEAR(x, expected_x, 1e-4); } INSTANTIATE_TEST_SUITE_P( Arm2D, KinematicsTest, ::testing::Values( std::make_tuple(0.0, 0.0, 1.0), std::make_tuple(M_PI/2, 0.0, 0.0), std::make_tuple(0.0, M_PI/2, 0.5) ));测试覆盖率多少才算够面试经常被问你们项目的测试覆盖率是多少先说个现实机器人项目的测试覆盖率普遍偏低。原因很简单——很多代码和硬件耦合太紧写测试的成本高。能做到核心算法模块80%以上覆盖率已经很不错了。但覆盖率不是越高越好。追求100%覆盖率会陷入为测而测的陷阱。重点测什么边界条件、异常路径、核心算法。PID控制器的积分饱和、卡尔曼滤波的协方差矩阵奇异、路径规划器的起点终点重合——这些边界情况才是测试的价值所在。覆盖率工具用gcov配合lcov就行。CI里每次构建跑覆盖率下降就报警这是比较成熟的做法。面试追问你们怎么保证测试不遗漏靠代码评审和测试设计。写新功能的时候PR里必须包含对应的测试代码reviewer检查测试是否覆盖了正常路径和异常路径。测试跑得太慢怎么办单元测试必须快——单个测试用例不超过100ms整个测试套件不超过30秒。如果慢了说明你把不该放在单元测试里的东西放进去了比如网络通信、文件IO。这些应该放到集成测试里。GTest和Catch2怎么选GTest生态更成熟ROS2官方用的就是GTest。Catch2是单头文件库集成更简单语法更现代。如果项目已经用了GTest就别换了新项目可以考虑Catch2。单元测试是代码质量的底线。没有测试的代码就是定时炸弹——现在跑着没问题改了一行代码三个模块同时崩。机器人系统复杂度越来越高靠人肉验证已经不够了。下一篇聊集成测试。单元测试保证每个模块没问题但模块拼在一起呢接口对得上吗数据格式一致吗这就是集成测试要解决的问题。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第326篇 嵌入式AI推理——TensorRT/NCNN的端侧部署下一篇预告第328篇 集成测试——多模块联调和接口测试有任何问题欢迎评论区留言我会尽量回复。