ROS 中的单元测试

1. 理论

在 ROS 中使用rostestgtest进行单元测试。


一、单元测试概述

  • 工具 :ROS 中的 rostest,基于 roslaunch 扩展,文件后缀为 .test,应放在 test/ 目录下。
  • 支持的框架
    • C++:Google Test(gtest
    • Python:nosetests

二、单元测试步骤

  1. 编写单元测试代码
  2. 纳入单元测试框架
  3. 编译单元测试
  4. 执行单元测试
  5. 查看单元测试结果

三、编写单元测试代码(gtest)

1. 测试结构

cpp 复制代码
TEST(TestSuite, testCase1) {
    // EXPECT_断言
    // ASSERT_断言
}
  • TEST 是基本单元,包含两个参数:
    • 测试套件(TestSuite):一组相关测试用例的集合
    • 测试用例(testCase1)

2. 断言类型

类别 断言宏示例 说明
布尔测试 EXPECT_TRUE, EXPECT_FALSE 判断布尔值
数值测试 EXPECT_EQ, EXPECT_NE, EXPECT_LT, EXPECT_GT 比较数值
字符串测试 EXPECT_STREQ, EXPECT_STRNE 比较字符串内容
浮点数测试 EXPECT_FLOAT_EQ, EXPECT_DOUBLE_EQ 比较浮点数(考虑精度)
  • ASSERT_:失败时立即退出当前测试用例
  • EXPECT_:失败时继续执行

四、纳入单元测试框架

1. 修改 CMakeLists.txt

cmake 复制代码
find_package(rostest REQUIRED)

if(CATKIN_ENABLE_TESTING)
    catkin_add_gtest(${PROJECT_NAME}_test test/example_100_test.cpp)
    target_link_libraries(${PROJECT_NAME}_test ${catkin_LIBRARIES})
endif()

2. 修改 package.xml

xml 复制代码
<build_depend>rostest</build_depend>

五、编译与执行单元测试

1. 编译命令

bash 复制代码
cd ~/catkin_ws/build/example_100
make run_tests

2. 执行测试(通过 .test 文件)

创建 .test 文件,例如 example_100.test

xml 复制代码
<launch>
    <test test-name="example_100_test" pkg="example_100" type="example_100_test" />
</launch>

执行命令:

bash 复制代码
rostest example_100 example_100.test

如需在终端输出文本模式结果:

bash 复制代码
rostest --text example_100 example_100.test

六、查看测试结果

  • 测试结果以 XML 格式 保存在 build/test_results/ 目录下,路径由 CATKIN_TEST_RESULTS_DIR 定义。
  • 查看汇总结果命令:
bash 复制代码
catkin_test_results test_results

示例输出:

复制代码
example_100/gtest-example_100_test.xml: 1 tests, 0 errors, 1 failures, 0 skipped
Summary: 1 tests, 0 errors, 1 failures, 0 skipped

七、示例测试代码(example_100_test.cpp)

cpp 复制代码
#include <ros/ros.h>
#include <gtest/gtest.h>

TEST(TestSuite, testCase1) {
    EXPECT_EQ(1, 1);
    EXPECT_EQ(1, 0);  // 会失败
}

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

编译运行后,失败信息会显示:

复制代码
[ FAILED ] TestSuite.testCase1
Expected equality of these values: 1 and 0

以下是完整的ROS单元测试项目结构和代码实现,包含所有必要的文件和运行命令:

2. 实践

一、完整目录结构

复制代码
~/catkin_ws/
├── src/
│   └── example_100/
│       ├── CMakeLists.txt
│       ├── package.xml
│       ├── include/
│       │   └── example_100/
│       │       └── hello_lib.h           # 头文件
│       ├── src/
│       │   ├── hello_lib.cpp              # 实现文件(无main)
│       │   └── hello.cpp                  # 主程序(只有main)
│       └── test/
│           ├── example_100_test.cpp       # 单元测试代码
│           └── example_100.test           # rostest启动文件
├── build/
│   └── example_100/                        # 编译生成目录
├── devel/                                   # 开发环境目录
└── logs/                                    # 日志目录(catkin build生成)
    └── test_results/                        # 测试结果存放目录

以及,

bash 复制代码
mkdir -p ~/catkin_ws/src/example_100/{src,test,include/example_100}
touch ~/catkin_ws/src/example_100/{CMakeLists.txt,package.xml}
touch ~/catkin_ws/src/example_100/include/example_100/hello_lib.h
touch ~/catkin_ws/src/example_100/src/{hello_lib.cpp,hello.cpp}
touch ~/catkin_ws/src/example_100/test/{example_100_test.cpp,example_100.test}

二、完整代码实现

1. package.xml

xml 复制代码
<?xml version="1.0"?>
<package format="2">
  <name>example_100</name>
  <version>0.0.1</version>
  <description>单元测试示例包</description>
  <maintainer email="user@example.com">user</maintainer>
  <license>BSD</license>

  <buildtool_depend>catkin</buildtool_depend>
  
  <depend>roscpp</depend>
  <depend>std_msgs</depend>
  
  <!-- 测试相关依赖 - catkin build 需要完整声明 -->
  <build_depend>rostest</build_depend>
  <build_export_depend>rostest</build_export_depend>
  <exec_depend>rostest</exec_depend>
  <test_depend>rostest</test_depend>
</package>

2. CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.0.2)
project(example_100)

## 查找依赖包
find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
  rostest
)

## 声明包信息
catkin_package(
  INCLUDE_DIRS include
  LIBRARIES ${PROJECT_NAME}_lib
  CATKIN_DEPENDS roscpp std_msgs
)

## 包含目录
include_directories(
  include
  ${catkin_INCLUDE_DIRS}
)

## 创建库文件(包含所有要测试的代码)
add_library(${PROJECT_NAME}_lib 
  src/hello_lib.cpp
)

## 编译可执行文件(只包含main函数)
add_executable(${PROJECT_NAME}_node 
  src/hello.cpp
)

## 链接库到可执行文件
target_link_libraries(${PROJECT_NAME}_node
  ${PROJECT_NAME}_lib
  ${catkin_LIBRARIES}
)

# 为了兼容性,也可以创建一个名为hello的别名
add_executable(hello ALIAS ${PROJECT_NAME}_node)

#############
## 单元测试 ##
#############
if(CATKIN_ENABLE_TESTING)
  # 添加gtest测试
  catkin_add_gtest(${PROJECT_NAME}_test 
    test/example_100_test.cpp
  )
  
  # 链接测试需要的库
  target_link_libraries(${PROJECT_NAME}_test 
    ${PROJECT_NAME}_lib  # 链接到同一个库
    ${catkin_LIBRARIES}
  )
  
  # 添加rostest测试(如果需要启动ROS节点)
  add_rostest(test/example_100.test)
endif()

3. 头文件 include/example_100/hello_lib.h

cpp 复制代码
#ifndef EXAMPLE_100_HELLO_LIB_H
#define EXAMPLE_100_HELLO_LIB_H

#include <string>

// 待测试的函数声明
int add(int a, int b);

// 待测试的类声明
class HelloWorld {
private:
    std::string message;
    
public:
    HelloWorld();
    std::string getMessage();
    bool setMessage(const std::string& msg);
    int multiply(int a, int b);
};

#endif

4. 实现文件 src/hello_lib.cpp

cpp 复制代码
#include <example_100/hello_lib.h>
#include <string>

// 待测试的函数
int add(int a, int b) {
    return a + b;
}

// 待测试的类实现
HelloWorld::HelloWorld() : message("Hello ROS!") {}

std::string HelloWorld::getMessage() {
    return message;
}

bool HelloWorld::setMessage(const std::string& msg) {
    if (msg.empty()) {
        return false;
    }
    message = msg;
    return true;
}

int HelloWorld::multiply(int a, int b) {
    return a * b;
}

5. 主程序文件 src/hello.cpp

cpp 复制代码
#include <ros/ros.h>
#include <example_100/hello_lib.h>

int main(int argc, char** argv) {
    ros::init(argc, argv, "hello_node");
    ros::NodeHandle nh;
    
    HelloWorld hello;
    ROS_INFO_STREAM(hello.getMessage());
    
    ROS_INFO_STREAM("5 + 3 = " << add(5, 3));
    ROS_INFO_STREAM("5 * 3 = " << hello.multiply(5, 3));
    
    return 0;
}

6. 单元测试代码 test/example_100_test.cpp

cpp 复制代码
#include <ros/ros.h>
#include <gtest/gtest.h>
#include <string>
#include <example_100/hello_lib.h>

// 测试套件:TestSuite
TEST(TestSuite, testAdd) {
    // 基本数值测试
    EXPECT_EQ(5, add(2, 3));
    EXPECT_EQ(0, add(-1, 1));
    EXPECT_EQ(-5, add(-2, -3));
    
    // 使用ASSERT检查关键条件
    ASSERT_NE(10, add(3, 4));
    
    // 其他数值比较
    EXPECT_LT(add(1, 1), 3);   // 小于
    EXPECT_GT(add(1, 1), 1);   // 大于
    EXPECT_LE(add(1, 1), 2);   // 小于等于
    EXPECT_GE(add(1, 1), 2);   // 大于等于
}

TEST(TestSuite, testMultiply) {
    HelloWorld hello;
    
    // 测试乘法函数
    EXPECT_EQ(6, hello.multiply(2, 3));
    EXPECT_EQ(0, hello.multiply(5, 0));
    EXPECT_EQ(-6, hello.multiply(-2, 3));
}

TEST(TestSuite, testStringMessage) {
    HelloWorld hello;
    
    // 测试字符串
    EXPECT_STREQ("Hello ROS!", hello.getMessage().c_str());
    EXPECT_STRNE("Hello World", hello.getMessage().c_str());
    
    // 测试setMessage
    EXPECT_TRUE(hello.setMessage("New Message"));
    EXPECT_STREQ("New Message", hello.getMessage().c_str());
    
    // 测试空字符串
    EXPECT_FALSE(hello.setMessage(""));
}

TEST(TestSuite, testBoolean) {
    HelloWorld hello;
    
    // 布尔测试
    EXPECT_TRUE(hello.setMessage("test"));
    EXPECT_FALSE(hello.setMessage(""));
    
    bool condition = (add(1, 1) == 2);
    EXPECT_TRUE(condition);
}

TEST(TestSuite, testFloat) {
    // 浮点数测试
    float a = 0.1f + 0.2f;
    float b = 0.3f;
    
    // 使用浮点数比较(允许微小误差)
    EXPECT_FLOAT_EQ(b, a);
    EXPECT_DOUBLE_EQ(0.3, 0.1 + 0.2);
    
    // 自定义精度比较
    EXPECT_NEAR(0.3, a, 0.0001);
}

// 测试套件:AdvancedTest
TEST(AdvancedTest, testComplex) {
    // 多个断言的组合测试
    int result = add(5, 5);
    
    EXPECT_EQ(10, result);
    EXPECT_NE(5, result);
    EXPECT_GT(result, 0);
    
    // 如果这个失败,整个测试用例退出
    ASSERT_EQ(10, result);
    
    // 如果上面ASSERT成功,这里继续执行
    HelloWorld hello;
    EXPECT_EQ(25, hello.multiply(5, 5));
}

// 带参数的主函数
int main(int argc, char** argv) {
    // 初始化gtest
    testing::InitGoogleTest(&argc, argv);
    
    // 初始化ROS(如果需要)
    ros::init(argc, argv, "test_node");
    
    // 运行所有测试
    return RUN_ALL_TESTS();
}

7. rostest启动文件 test/example_100.test

xml 复制代码
<launch>
    <!-- 可选的ROS节点启动 -->
    <node name="hello_node" pkg="example_100" type="example_100_node" output="screen" unless="$(optenv IS_TESTING)"/>
    
    <!-- 单元测试节点 -->
    <test test-name="example_100_test" 
          pkg="example_100" 
          type="example_100_test" 
          time-limit="10.0"           <!-- 测试超时限制 -->
          retry="2"                    <!-- 失败重试次数 -->
          output="screen"/>             <!-- 输出到屏幕 -->
    
    <!-- 可选的测试参数 -->
    <param name="/test_param" value="test_value" />
</launch>

三、完整运行命令(适配 catkin build)

1. 首次编译和测试

bash 复制代码
# 进入工作空间
cd ~/catkin_ws

# 初始化工作空间(如果是第一次使用catkin build)
catkin init

# 编译整个工作空间
catkin build

# 或者只编译example_100包
catkin build example_100

# 运行所有测试
catkin run_tests

# 运行特定包的测试
catkin run_tests example_100

# 编译并运行测试(一步完成)
catkin build example_100 --catkin-make-args run_tests

2. 使用rostest命令

bash 复制代码
# 进入工作空间
cd ~/catkin_ws

# 设置环境变量
source devel/setup.bash

# 运行rostest(带输出)
rostest example_100 example_100.test

# 文本模式运行(输出详细信息)
rostest --text example_100 example_100.test

# 重复运行测试
rostest --repeats 3 example_100 example_100.test

3. 查看测试结果

bash 复制代码
# 查看所有测试结果汇总(catkin build会将测试结果放在logs目录)
catkin_test_results

# 查看指定构建空间的测试结果
catkin_test_results ~/catkin_ws/logs/test_results

# 查看详细测试结果
catkin_test_results --verbose

# 查看特定包的测试结果
catkin_test_results ~/catkin_ws/logs/test_results/example_100

# 查看XML格式的测试结果
cat ~/catkin_ws/logs/test_results/example_100/gtest-example_100_test.xml

# 使用catkin build自带的测试结果查看命令
catkin build --no-deps example_100 --catkin-make-args test_results

4. 常用测试命令组合

bash 复制代码
# 清理并重新测试
cd ~/catkin_ws
catkin clean example_100 -b     # 只清理build目录
catkin build example_100
catkin run_tests example_100

# 完全清理(包括devel和logs)
catkin clean example_100 -a

# 只运行测试(不重新编译)
cd ~/catkin_ws
catkin run_tests example_100

# 运行测试并查看详细输出
cd ~/catkin_ws
catkin run_tests example_100 --no-deps --interleave

# 运行特定测试用例(使用gtest过滤)
rostest --text example_100 example_100.test --gtest_filter=TestSuite.testAdd

# 在测试失败时进入调试模式
rostest --text example_100 example_100.test --gtest_break_on_failure

5. catkin build 特有的测试命令

bash 复制代码
# 查看可用的测试目标
catkin list --test-deps example_100

# 以并行方式运行测试
catkin run_tests -j4

# 只运行失败的测试
catkin run_tests --no-deps --failed

# 查看测试状态
catkin build --verbose --no-deps example_100 --catkin-make-args test

# 生成测试覆盖率报告(如果配置了)
catkin build example_100 --catkin-make-args coverage

6. 预期输出示例

复制代码
运行测试时的输出(catkin run_tests):
[build] Starting build with 1 job
[build] Package example_100 constructed in 0.1 seconds
[build] Running tests for package 'example_100'
[test] Starting tests...
[test] Running command: rostest example_100 example_100.test

[ RUN      ] TestSuite.testAdd
[       OK ] TestSuite.testAdd (1 ms)
[ RUN      ] TestSuite.testMultiply
[       OK ] TestSuite.testMultiply (0 ms)
[ RUN      ] TestSuite.testStringMessage
[       OK ] TestSuite.testStringMessage (1 ms)
[ RUN      ] TestSuite.testBoolean
[       OK ] TestSuite.testBoolean (0 ms)
[ RUN      ] TestSuite.testFloat
[       OK ] TestSuite.testFloat (0 ms)
[ RUN      ] AdvancedTest.testComplex
[       OK ] AdvancedTest.testComplex (0 ms)

[test] Tests finished successfully.
[test] Test results stored in: ~/catkin_ws/logs/test_results/example_100

测试结果汇总(catkin_test_results):
Summary: 6 tests, 0 errors, 0 failures, 0 skipped
example_100: gtest-example_100_test.xml (6 tests, 0 errors, 0 failures, 0 skipped)

All test results stored in: /home/user/catkin_ws/logs/test_results

如果测试失败,会看到类似:

复制代码
[ FAILED ] TestSuite.testAdd
Expected equality of these values: 5 and 3

测试结果汇总(有失败):
Summary: 6 tests, 0 errors, 1 failures, 0 skipped
example_100: gtest-example_100_test.xml (6 tests, 0 errors, 1 failures, 0 skipped)

7. 调试测试失败的技巧

bash 复制代码
# 以文本模式运行,看到详细输出
rostest --text example_100 example_100.test

# 只运行失败的测试用例
rostest --text example_100 example_100.test --gtest_filter=TestSuite.testAdd

# 重复运行直到失败(查找偶发故障)
rostest --repeats 100 --text example_100 example_100.test

# 在第一个失败时停止
rostest --text example_100 example_100.test --gtest_break_on_failure

# 查看详细的XML测试报告
cat ~/catkin_ws/logs/test_results/example_100/gtest-example_100_test.xml

这样就完成了适配 catkin build 的完整ROS单元测试项目的所有文件内容和运行命令。

相关推荐
*.✧屠苏隐遥(ノ◕ヮ◕)ノ*.✧2 小时前
Day01 Junit 单元测试 & 反射
java·后端·junit·单元测试
visual_zhang15 小时前
单元测试系列:如何测试不愿暴露的私有状态
单元测试
金銀銅鐵3 天前
浅解 JUnit 4 第十五篇:如何在测试方法运行前后做些事情?
junit·单元测试
金銀銅鐵4 天前
浅解 JUnit 4 第十四篇:如何实现一个 @After 注解的替代品?
junit·单元测试
金銀銅鐵4 天前
浅解 JUnit 4 第十三篇:如何实现一个 @Before 注解的替代品?(下)
junit·单元测试
金銀銅鐵7 天前
浅解 JUnit 4 第十二篇:如何生成 @Before 注解的替代品?(上)
junit·单元测试
Apifox7 天前
【测试套件】当用户说“我只想跑 P0 用例”时,我们到底在说什么
单元测试·测试·ab测试
金銀銅鐵10 天前
浅解 JUnit 4 第十一篇:@Before 注解和 @After 注解如何发挥作用?
junit·单元测试
金銀銅鐵12 天前
浅解 JUnit 4 第十篇:方法上的 @Ignore 注解
junit·单元测试