> ## 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

> Bring up multiple ROS2 nodes from a single test using launch_testing helpers from the Roboticks SDK. System-test patterns and a complete launch description.

# 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.

<Info>
  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.
</Info>

## The three helpers

| Helper                                                  | Purpose                                                                                                         |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `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:

```python theme={null}
from roboticks.launch_testing import (
    make_node_action,
    generate_test_description,
    spin_node,
)
```

Full signatures in the [SDK module map](/sdk/modules).

## Integration vs system tests

| Pattern              | Bring up                              | Assert via                                           | Typical fixture             |
| -------------------- | ------------------------------------- | ---------------------------------------------------- | --------------------------- |
| **Integration test** | One node, in-process                  | `assert_topic_published` from `roboticks.assertions` | pytest fixture              |
| **System test**      | Two-or-more nodes, separate processes | Subscribed observers + launch\_testing assertions    | `generate_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.

```python theme={null}
# 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()`:

```python theme={null}
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](/testing/sim-runners).

## Next

<CardGroup cols={2}>
  <Card title="Fault injection" icon="bug" href="/testing/fault-injection">
    Inject faults into the running system from inside a launch test.
  </Card>

  <Card title="MCAP capture" icon="floppy-disk" href="/testing/mcap-capture">
    Record the launched graph for replay during failure triage.
  </Card>

  <Card title="Sim runners" icon="microchip" href="/testing/sim-runners">
    Pick Gazebo Harmonic or Webots; control hosted vs self-hosted GPU pools.
  </Card>

  <Card title="Pytest tests" icon="python" href="/testing/writing-tests-pytest">
    For the simpler in-process case.
  </Card>
</CardGroup>
