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

> Use @confirms, @tags, @deadline, and @requires_sim with rclpy assertion helpers to write traceable ROS2 tests. Full examples and conftest patterns.

# Writing tests in pytest

Pytest is the canonical way to write Python tests for Roboticks. The [`roboticks`](https://pypi.org/project/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

| Decorator                       | Purpose                                                  | Example                             |
| ------------------------------- | -------------------------------------------------------- | ----------------------------------- |
| `@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](/sdk/decorators) for full semantics.

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

<Info>
  **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](/sdk/pytest-plugin).
</Info>

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

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

### Subscribe-and-assert on `/cmd_vel`

```python theme={null}
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

```python theme={null}
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

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

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

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

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

```xml theme={null}
<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](/testing/wire-contract) for the full schema and version handshake.

## Next

<CardGroup cols={2}>
  <Card title="Fault injection" icon="bug" href="/testing/fault-injection">
    Drop topics, delay messages, kill nodes — under a context manager.
  </Card>

  <Card title="MCAP capture" icon="floppy-disk" href="/testing/mcap-capture">
    Record bag files per test for failure forensics.
  </Card>

  <Card title="Launch testing" icon="rocket" href="/testing/launch-testing">
    System tests that bring up multiple nodes.
  </Card>

  <Card title="Decorator reference" icon="book" href="/sdk/decorators">
    Full signatures and edge cases.
  </Card>
</CardGroup>
