Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.roboticks.io/llms.txt

Use this file to discover all available pages before exploring further.

Launch testing

launch_testing is the ROS2 standard for system tests — tests that bring up real nodes, real topics, real lifecycle transitions, and assert on the outside-the-box behaviour. The Roboticks SDK ships three thin helpers to make the boilerplate tolerable.
Use pytest with rclpy helpers for unit and integration tests. Use launch_testing when the thing under test is a graph of nodes and you need to assert across their boundaries.

The three helpers

HelperPurpose
make_node_action(package, executable, **kwargs)Builds a launch_ros.actions.Node with sensible defaults (output to screen, parameters from dict, namespaced).
generate_test_description(*actions, ready_event=None)Wraps a list of actions in the dance launch_testing demands: launch description, ready-fn, return-tuple.
spin_node(node, *, within)Spins a single rclpy node for a bounded duration, useful inside the post_shutdown phase.
Imports:
from roboticks.launch_testing import (
    make_node_action,
    generate_test_description,
    spin_node,
)
Full signatures in the SDK module map.

Integration vs system tests

PatternBring upAssert viaTypical fixture
Integration testOne node, in-processassert_topic_published from roboticks.assertionspytest fixture
System testTwo-or-more nodes, separate processesSubscribed observers + launch_testing assertionsgenerate_test_description
The cost is real: system tests start at ~3 s overhead per test. Reserve them for behaviour that only manifests across process boundaries — IPC failure, lifecycle ordering, parameter propagation under a real launch file.

A complete example: navigation stack handshake

This test brings up the planner and controller, sends a goal, and asserts the controller publishes /cmd_vel within 5 seconds.
# test/system/test_nav_handshake.py
import unittest
import pytest
import launch_testing.actions
import rclpy
from geometry_msgs.msg import Twist
from roboticks import confirms, requires_sim
from roboticks.launch_testing import (
    make_node_action,
    generate_test_description,
)


@pytest.mark.launch_test
def generate_test_description_fn():
    planner = make_node_action(
        package="nav2_planner",
        executable="planner_server",
        parameters={"use_sim_time": True},
    )
    controller = make_node_action(
        package="nav2_controller",
        executable="controller_server",
        parameters={"use_sim_time": True},
    )
    return generate_test_description(
        planner,
        controller,
        launch_testing.actions.ReadyToTest(),
    )


@confirms("REQ-031")
@requires_sim("gazebo")
class TestNavHandshake(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        rclpy.init()
        cls.node = rclpy.create_node("test_nav_observer")
        cls.received: list[Twist] = []
        cls.sub = cls.node.create_subscription(
            Twist, "/cmd_vel",
            lambda m: cls.received.append(m), 10,
        )

    @classmethod
    def tearDownClass(cls):
        cls.node.destroy_node()
        rclpy.shutdown()

    def test_controller_emits_cmd_vel(self):
        deadline = self.node.get_clock().now().nanoseconds + 5_000_000_000
        while self.node.get_clock().now().nanoseconds < deadline:
            rclpy.spin_once(self.node, timeout_sec=0.1)
            if self.received:
                break
        self.assertGreater(len(self.received), 0,
            "controller never published /cmd_vel within 5 s")

Why the helpers exist

Vanilla launch_testing makes you assemble four things every test:
  1. A LaunchDescription of Node actions.
  2. A ReadyToTest() sentinel.
  3. A pytest_marker hook.
  4. A return (description, context_dict) tuple.
generate_test_description() collapses that into one call. make_node_action() collapses the per-node arg-soup. spin_node() saves you re-typing the rclpy executor dance in post_shutdown.

post_shutdown assertions

For checks that need the nodes gone (clean shutdown, log content, no orphaned processes), use launch_testing.post_shutdown_test():
import launch_testing

@launch_testing.post_shutdown_test()
class TestCleanShutdown(unittest.TestCase):
    def test_no_zombie_processes(self, proc_info):
        launch_testing.asserts.assertExitCodes(proc_info)

Routing to a runner

A launch_testing test ID is <file>::generate_test_description::<TestClass>::<test_method>. The Roboticks scheduler treats it like any other pytest test for routing — @requires_sim on the class (or the function) tells the job router to land on a sim runner. See Sim runners.

Next

Fault injection

Inject faults into the running system from inside a launch test.

MCAP capture

Record the launched graph for replay during failure triage.

Sim runners

Pick Gazebo Harmonic or Webots; control hosted vs self-hosted GPU pools.

Pytest tests

For the simpler in-process case.