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.

Writing tests in pytest

Pytest is the canonical way to write Python tests for Roboticks. The roboticks package adds four decorators and a set of rclpy-aware assertion helpers on top of stock pytest. Nothing else changes — your fixtures, your conftest.py, your parametrize idioms keep working.

The four decorators

DecoratorPurposeExample
@confirms(*req_ids)Records which requirement IDs this test verifies@confirms("REQ-014", "REQ-022")
@tags(*tags)Free-form labels for filtering (e.g. smoke, nightly)@tags("nightly", "perception")
@deadline(milliseconds=int)Fails the test if it exceeds the wall-clock budget@deadline(milliseconds=100)
@requires_sim(engine, *, gpu)Routes the test to a sim-capable runner@requires_sim("gazebo", gpu=True)
All four stack and can decorate the same test in any order. See the SDK decorator reference for full semantics.
from roboticks import confirms, tags, deadline, requires_sim

@confirms("REQ-014", "REQ-022")
@tags("nightly", "navigation")
@requires_sim("gazebo", gpu=True)
@deadline(milliseconds=2000)
def test_robot_avoids_static_obstacle():
    ...
Decorators are inert without the platform. Locally they only mark functions. The pytest plugin reads them at collection time and writes them into JUnit XML on session-end. See Pytest plugin.

Rclpy assertion helpers

The SDK ships ROS2-aware assertions in roboticks.assertions. They spin a node briefly, wait for the predicate, and raise an informative AssertionError on timeout. The helpers are guarded: importing the module on a host without rclpy installed raises a clear RuntimeError instead of an opaque ImportError.
from roboticks.assertions import (
    assert_topic_published,
    assert_service_response,
    assert_action_result,
    assert_param_equals,
    assert_tf_transform,
)
Full signatures live in the SDK assertion reference.

Subscribe-and-assert on /cmd_vel

import pytest
from geometry_msgs.msg import Twist
from roboticks import confirms, deadline
from roboticks.assertions import assert_topic_published

@confirms("REQ-007")
@deadline(milliseconds=5000)
def test_teleop_publishes_cmd_vel(ros_context):
    # ros_context is your conftest fixture that has rclpy.init()'d
    msg = assert_topic_published(
        topic="/cmd_vel",
        msg_type=Twist,
        within=3.0,                       # seconds
        predicate=lambda m: m.linear.x > 0.1,
    )
    assert msg.linear.x > 0.1
    assert msg.angular.z == pytest.approx(0.0, abs=0.01)

Service call response

from example_interfaces.srv import AddTwoInts
from roboticks import confirms
from roboticks.assertions import assert_service_response

@confirms("REQ-019")
def test_add_two_ints_service(ros_context):
    request = AddTwoInts.Request(a=2, b=3)
    response = assert_service_response(
        service="/add_two_ints",
        srv_type=AddTwoInts,
        request=request,
        within=2.0,
    )
    assert response.sum == 5

Action result

from nav2_msgs.action import NavigateToPose
from roboticks import confirms, requires_sim
from roboticks.assertions import assert_action_result

@confirms("REQ-031")
@requires_sim("gazebo")
def test_nav_to_pose_reaches_goal(ros_context):
    goal = NavigateToPose.Goal()
    goal.pose.pose.position.x = 2.0
    result = assert_action_result(
        action="/navigate_to_pose",
        action_type=NavigateToPose,
        goal=goal,
        within=45.0,
    )
    assert result.result.error_code == 0

Conftest pattern

Spin up rclpy once per test session, and once per test give every test a clean executor. This pattern works for unit-scope ROS tests; for system tests use launch_testing instead.
# conftest.py
import pytest
import rclpy
from rclpy.executors import SingleThreadedExecutor

@pytest.fixture(scope="session", autouse=True)
def _ros():
    rclpy.init()
    yield
    rclpy.shutdown()

@pytest.fixture
def ros_context():
    executor = SingleThreadedExecutor()
    yield executor
    executor.shutdown()
For tests that need a node-under-test running, layer it on:
@pytest.fixture
def perception_node(ros_context):
    from my_pkg.perception_node import PerceptionNode
    node = PerceptionNode()
    ros_context.add_node(node)
    yield node
    node.destroy_node()

Parametrize and @confirms

@confirms is per-test-function, not per-test-id. Parametrized variants all confirm the same requirement set — which is usually what you want:
import pytest
from roboticks import confirms

@confirms("REQ-042")
@pytest.mark.parametrize("velocity", [0.1, 0.5, 1.0, 2.0])
def test_velocity_clamp(velocity):
    ...
If a parametrized branch should confirm a different requirement, split it into two test functions. Decorators are deliberately not parametrize-aware — that ambiguity is what got teams in trouble with older tools.

What the plugin emits

Per test, in the JUnit XML:
<testcase name="test_estop_halts_motion" classname="tests.test_estop" time="0.082">
  <properties>
    <property name="roboticks.confirms" value="REQ-001,REQ-014"/>
    <property name="roboticks.tags" value="safety,smoke"/>
    <property name="roboticks.deadline_ms" value="100"/>
  </properties>
</testcase>
See Wire contract for the full schema and version handshake.

Next

Fault injection

Drop topics, delay messages, kill nodes — under a context manager.

MCAP capture

Record bag files per test for failure forensics.

Launch testing

System tests that bring up multiple nodes.

Decorator reference

Full signatures and edge cases.