Skip to content

集成测试#

集成测试被定义为软件测试中的阶段,其中各个软件模块作为一个组进行组合和测试. 集成测试发生在单元测试之后和验证测试之前.

集成测试的输入是一组已经过单元测试的独立模块. 模块集根据定义的集成测试计划进行测试, 输出是一组正确集成的软件模块,可用于系统测试.

集成测试的价值#

集成测试确定独立开发的软件模块在模块相互连接时是否正常工作. 在 ROS 2 中,软件模块称为节点. 测试单个节点是一种特殊类型的集成测试,通常称为组件测试.

集成测试有助于查找以下类型的错误:

  • 节点之间的交互不兼容,例如不匹配的主题、不同的消息类型或 QoS 设置不兼容.
  • 单元测试未涉及的边缘情况,例如关键计时问题、网络通信延迟、磁盘 I/O 故障以及生产环境中可能发生的其他此类问题.
  • 当系统处于高 CPU/内存负载下时可能发生的问题,例如 malloc 故障.这可以使用 stressudpreplay 等工具进行测试,以测试具有真实数据的节点的性能.

使用 ROS 2,可以对具有大量节点的复杂自动驾驶应用程序进行编程. 因此,已经付出了很多努力来提供一个集成测试框架,帮助开发人员测试 ROS 2 节点的交互.

集成测试框架#

典型的集成测试框架包含三个部分:

  1. 一系列带有参数的可执行文件,它们协同工作并生成输出.
  2. 一系列预期输出,应与可执行文件的输出匹配.
  3. 启动器,用于启动测试,将输出与预期输出进行比较,并确定测试是否通过.

在 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()

这样做会增加冒烟测试,以确保节点可以:

  1. 使用默认参数文件启动.
  2. 以标准的 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 中执行的不同类型的测试以及指向相应指南的链接.