1. 理论
在 ROS 中使用rostest和gtest进行单元测试。
一、单元测试概述
- 工具 :ROS 中的
rostest,基于roslaunch扩展,文件后缀为.test,应放在test/目录下。 - 支持的框架 :
- C++:Google Test(
gtest) - Python:
nosetests
- C++:Google Test(
二、单元测试步骤
- 编写单元测试代码
- 纳入单元测试框架
- 编译单元测试
- 执行单元测试
- 查看单元测试结果
三、编写单元测试代码(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单元测试项目的所有文件内容和运行命令。