集成测试#
集成测试被定义为软件测试中的阶段,其中各个软件模块作为一个组进行组合和测试. 集成测试发生在单元测试之后和验证测试之前.
集成测试的输入是一组已经过单元测试的独立模块. 模块集根据定义的集成测试计划进行测试, 输出是一组正确集成的软件模块,可用于系统测试.
集成测试的价值#
集成测试确定独立开发的软件模块在模块相互连接时是否正常工作. 在 ROS 2 中,软件模块称为节点. 测试单个节点是一种特殊类型的集成测试,通常称为组件测试.
集成测试有助于查找以下类型的错误:
- 节点之间的交互不兼容,例如不匹配的主题、不同的消息类型或 QoS 设置不兼容.
- 单元测试未涉及的边缘情况,例如关键计时问题、网络通信延迟、磁盘 I/O 故障以及生产环境中可能发生的其他此类问题.
- 当系统处于高 CPU/内存负载下时可能发生的问题,例如
malloc故障.这可以使用stress和udpreplay等工具进行测试,以测试具有真实数据的节点的性能.
使用 ROS 2,可以对具有大量节点的复杂自动驾驶应用程序进行编程. 因此,已经付出了很多努力来提供一个集成测试框架,帮助开发人员测试 ROS 2 节点的交互.
集成测试框架#
典型的集成测试框架包含三个部分:
- 一系列带有参数的可执行文件,它们协同工作并生成输出.
- 一系列预期输出,应与可执行文件的输出匹配.
- 启动器,用于启动测试,将输出与预期输出进行比较,并确定测试是否通过.
在 Autoware 中,我们使用 launch_testing 框架.
烟雾测试#
Autoware 具有用于冒烟测试的专用 API.
要使用此框架,请在 package.xml 中添加:
<test_depend>autoware_testing</test_depend>
在 CMakeLists.txt 中添加:
if(BUILD_TESTING)
find_package(autoware_testing REQUIRED)
add_smoke_test(${PROJECT_NAME} ${NODE_NAME})
endif()
这样做会增加冒烟测试,以确保节点可以:
- 使用默认参数文件启动.
- 以标准的
SIGTERM信号终止.
有关完整的 API 文档, 请参考 Package Design Page.
注意
此 API 并不适合所有冒烟测试用例. 当需要将特定文件位置(例如:用于 map)传递给节点时,或者需要在节点启动前进行一些准备时,不能使用它. 在这种情况下,请使用 下面的组件测试部分 中的手动解决方案.
单节点集成测试:组件测试#
最简单的方案是单个节点. 在这种情况下,集成测试通常称为组件测试.
要将组件测试添加到现有节点,
您可以按照 autoware_map_loader 包 中的 lanelet2_map_loader 为例
(在 此 PR 中添加).
在 package.xml 中,添加:
<test_depend>ros_testing</test_depend>
在 CMakeLists.txt 中,
添加或修改 BUILD_TESTING 部分:
if(BUILD_TESTING)
add_ros_test(
test/lanelet2_map_loader_launch.test.py
TIMEOUT "30"
)
install(DIRECTORY
test/data/
DESTINATION share/${PROJECT_NAME}/test/data/
)
endif()
除了命令 add_ros_test 之外,我们还使用 install 命令安装测试所需的任何数据.
注意
TIMEOUT参数以秒为单位;有关详细信息,请参见 add_ros_test.cmake 文件.add_ros_test命令将以唯一的ROS_DOMAIN_ID运行测试,从而避免并行运行的测试之间的干扰.
要创建测试, 请阅读 launch_testing 快速入门示例, 或按照以下步骤作.
以 test/lanelet2_map_loader_launch.test.py 为例,
首先导入依赖项:
import os
import unittest
from ament_index_python import get_package_share_directory
import launch
from launch import LaunchDescription
from launch_ros.actions import Node
import launch_testing
import pytest
然后,创建启动描述以启动受测节点.
请注意,找到 test_map.osm 文件路径并将其传递给节点
使用 冒烟测试 API 无法完成的事情:
@pytest.mark.launch_test
def generate_test_description():
lanelet2_map_path = os.path.join(
get_package_share_directory("autoware_map_loader"), "test/data/test_map.osm"
)
lanelet2_map_loader = Node(
package="autoware_map_loader",
executable="autoware_lanelet2_map_loader",
parameters=[{"lanelet2_map_path": lanelet2_map_path}],
)
context = {}
return (
LaunchDescription(
[
lanelet2_map_loader,
# Start test after 1s - gives time for the map_loader to finish initialization
launch.actions.TimerAction(
period=1.0, actions=[launch_testing.actions.ReadyToTest()]
),
]
),
context,
)
注意
- 由于节点需要时间来处理输入的 lanelet2 map,我们使用 TimerAction 将测试的开始时间推迟 1s.
- 在上面的示例中,
context是空的,但它可用于将对象传递给测试用例. - 您可以在 ROS 2 context_launch_test.py 测试示例中找到使用
context的示例.
最后,在节点可执行文件关闭后执行测试 (post_shutdown_test).
在这里,我们确保节点启动没有错误并干净地退出.
@launch_testing.post_shutdown_test()
class TestProcessOutput(unittest.TestCase):
def test_exit_code(self, proc_info):
# Check that process exits with code 0: no error
launch_testing.asserts.assertExitCodes(proc_info)
运行测试#
继续上面的示例,首先构建您的包:
colcon build --packages-up-to autoware_map_loader
source install/setup.bash
然后手动执行组件测试:
ros2 test src/universe/autoware_universe/map/autoware_map_loader/test/lanelet2_map_loader_launch.test.py
或者作为测试整个包的一部分:
colcon test --packages-select autoware_map_loader
验证测试是否已执行;例如
$ colcon test-result --all --verbose
...
build/autoware_map_loader/test_results/autoware_map_loader/test_lanelet2_map_loader_launch.test.py.xunit.xml: 1 test, 0 errors, 0 failures, 0 skipped
下一步#
使用单个节点的集成测试:组件测试 中描述的简单测试可以扩展到多个方向,例如测试节点的输出.
测试节点的输出#
要在节点运行时进行测试,
通过添加 https://github.com/ros2/launch/tree/foxy/launch_testing#active-tests_active test_Python 的 unittest.TestCase 设置为 *launch.test.py
通过创建节点和特定主题的订阅来访问输出需要一些样板代码,例如
import unittest
class TestRunningDataPublisher(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.context = Context()
rclpy.init(context=cls.context)
cls.node = rclpy.create_node("test_node", context=cls.context)
@classmethod
def tearDownClass(cls):
rclpy.shutdown(context=cls.context)
def setUp(self):
self.msgs = []
sub = self.node.create_subscription(
msg_type=my_msg_type,
topic="/info_test",
callback=self._msg_received
)
self.addCleanup(self.node.destroy_subscription, sub)
def _msg_received(self, msg):
# Callback for ROS 2 subscriber used in the test
self.msgs.append(msg)
def get_message(self):
startlen = len(self.msgs)
executor = rclpy.executors.SingleThreadedExecutor(context=self.context)
executor.add_node(self.node)
try:
# Try up to 60 s to receive messages
end_time = time.time() + 60.0
while time.time() < end_time:
executor.spin_once(timeout_sec=0.1)
if startlen != len(self.msgs):
break
self.assertNotEqual(startlen, len(self.msgs))
return self.msgs[-1]
finally:
executor.remove_node(self.node)
def test_message_content():
msg = self.get_message()
self.assertEqual(msg, "Hello, world")
参考资料#
- colcon 用于构建和运行测试.
- launch testing 启动节点并运行测试.
- 测试指南 描述了在 Autoware 中执行的不同类型的测试以及指向相应指南的链接.