{"id":"53298d44-6a5f-480e-8d4f-472f47e87334","shortId":"bjcs6E","kind":"skill","title":"robotics-testing","tagline":"Testing strategies, patterns, and tools for robotics software. Use this skill when writing unit tests, integration tests, simulation tests, or hardware-in-the-loop tests for robot systems. Trigger whenever the user mentions testing ROS nodes, pytest with ROS, launch_testing, simu","description":"# Robotics Testing Skill\n\n## When to Use This Skill\n- Writing unit tests for ROS1/ROS2 nodes\n- Setting up integration tests with launch_testing\n- Mocking hardware (sensors, actuators) for CI/CD\n- Building simulation-based test suites\n- Testing perception pipelines with ground truth\n- Validating trajectory planners and controllers\n- Setting up CI/CD pipelines for robotics packages\n- Debugging flaky tests in robotics systems\n\n## The Robotics Testing Pyramid\n\n```\n                    ╱╲\n                   ╱  ╲        Field Tests\n                  ╱    ╲       (Real robot, real environment)\n                 ╱──────╲\n                ╱        ╲     Hardware-in-the-Loop (HIL)\n               ╱          ╲    (Real hardware, controlled environment)\n              ╱────────────╲\n             ╱              ╲   Simulation Tests\n            ╱                ╲  (Full sim, realistic physics)\n           ╱──────────────────╲\n          ╱                    ╲  Integration Tests\n         ╱                      ╲ (Multi-node, message passing)\n        ╱────────────────────────╲\n       ╱                          ╲ Unit Tests\n      ╱____________________________╲ (Single function/class, fast, deterministic)\n\nMORE tests at the bottom, FEWER at the top.\nBottom = fast, cheap, deterministic. Top = slow, expensive, realistic.\n```\n\n## Unit Testing Patterns\n\n### Testing ROS2 Nodes with pytest\n\n```python\n# test_perception_node.py\nimport pytest\nimport rclpy\nfrom rclpy.node import Node\nfrom sensor_msgs.msg import Image\nfrom my_pkg.perception_node import PerceptionNode\nimport numpy as np\n\n@pytest.fixture(scope='module')\ndef ros_context():\n    \"\"\"Initialize ROS2 context once per test module\"\"\"\n    rclpy.init()\n    yield\n    rclpy.shutdown()\n\n@pytest.fixture\ndef perception_node(ros_context):\n    \"\"\"Create a fresh perception node for each test\"\"\"\n    node = PerceptionNode()\n    yield node\n    node.destroy_node()\n\n@pytest.fixture\ndef test_image():\n    \"\"\"Generate a synthetic test image\"\"\"\n    msg = Image()\n    msg.height = 256\n    msg.width = 256\n    msg.encoding = 'rgb8'\n    msg.step = 256 * 3\n    msg.data = np.random.randint(0, 255, (256, 256, 3),\n                                  dtype=np.uint8).tobytes()\n    return msg\n\nclass TestPerceptionNode:\n\n    def test_initialization(self, perception_node):\n        \"\"\"Node should initialize with correct default parameters\"\"\"\n        assert perception_node.get_parameter('confidence_threshold').value == 0.7\n        assert perception_node.get_parameter('rate_hz').value == 30.0\n\n    def test_parameter_validation(self, perception_node):\n        \"\"\"Node should reject invalid parameter values\"\"\"\n        from rcl_interfaces.msg import SetParametersResult\n        result = perception_node.set_parameters([\n            rclpy.parameter.Parameter('confidence_threshold',\n                                       value=-0.5)  # Invalid!\n        ])\n        assert not result[0].successful\n\n    def test_image_callback_publishes_detections(self, perception_node, test_image):\n        \"\"\"Processing an image should produce detection output\"\"\"\n        received = []\n\n        # Create a test subscriber\n        sub_node = Node('test_subscriber')\n        sub_node.create_subscription(\n            DetectionArray, '/perception/detections',\n            lambda msg: received.append(msg), 10)\n\n        # Simulate image callback\n        perception_node.image_callback(test_image)\n\n        # Spin briefly to allow message propagation\n        rclpy.spin_once(sub_node, timeout_sec=1.0)\n        rclpy.spin_once(perception_node, timeout_sec=1.0)\n\n        # Verify\n        assert len(received) > 0\n        sub_node.destroy_node()\n\n    def test_empty_image_handling(self, perception_node):\n        \"\"\"Node should handle empty/corrupted images gracefully\"\"\"\n        empty_msg = Image()  # No data\n        # Should not crash\n        perception_node.image_callback(empty_msg)\n```\n\n### Testing Pure Functions (No ROS Dependency)\n\n```python\n# test_kinematics.py\nimport pytest\nimport numpy as np\nfrom my_pkg.kinematics import (\n    forward_kinematics, inverse_kinematics,\n    quaternion_multiply, transform_point\n)\n\nclass TestForwardKinematics:\n\n    @pytest.mark.parametrize(\"joint_angles,expected_pos\", [\n        # Home position\n        (np.zeros(7), np.array([0.088, 0.0, 1.033])),\n        # Known calibrated pose\n        (np.array([0, -0.785, 0, -2.356, 0, 1.571, 0.785]),\n         np.array([0.307, 0.0, 0.59])),\n    ])\n    def test_known_poses(self, joint_angles, expected_pos):\n        \"\"\"FK should match known calibrated positions\"\"\"\n        result = forward_kinematics(joint_angles)\n        np.testing.assert_allclose(result[:3], expected_pos, atol=0.01)\n\n    def test_fk_ik_roundtrip(self):\n        \"\"\"FK(IK(pose)) should return the original pose\"\"\"\n        original_pose = np.array([0.4, 0.1, 0.5, 1.0, 0.0, 0.0, 0.0])\n        joint_angles = inverse_kinematics(original_pose)\n        recovered_pose = forward_kinematics(joint_angles)\n        np.testing.assert_allclose(recovered_pose, original_pose, atol=1e-4)\n\n    def test_joint_limits_respected(self):\n        \"\"\"IK should not return angles outside joint limits\"\"\"\n        target = np.array([0.5, 0.2, 0.3, 1.0, 0.0, 0.0, 0.0])\n        joints = inverse_kinematics(target)\n        for i, (lo, hi) in enumerate(JOINT_LIMITS):\n            assert lo <= joints[i] <= hi, \\\n                f\"Joint {i}: {joints[i]} outside [{lo}, {hi}]\"\n\n\nclass TestQuaternionMath:\n\n    def test_identity_multiply(self):\n        \"\"\"q * identity = q\"\"\"\n        q = np.array([0.5, 0.5, 0.5, 0.5])\n        identity = np.array([1.0, 0.0, 0.0, 0.0])\n        result = quaternion_multiply(q, identity)\n        np.testing.assert_allclose(result, q, atol=1e-10)\n\n    def test_inverse_multiply(self):\n        \"\"\"q * q_inv = identity\"\"\"\n        q = np.array([0.5, 0.5, 0.5, 0.5])\n        q_inv = np.array([0.5, -0.5, -0.5, -0.5])\n        result = quaternion_multiply(q, q_inv)\n        np.testing.assert_allclose(result, [1, 0, 0, 0], atol=1e-10)\n\n    @pytest.mark.parametrize(\"q\", [\n        np.random.randn(4) for _ in range(20)  # Random quaternions\n    ])\n    def test_unit_quaternion_preserved(self, q):\n        \"\"\"Multiplication of unit quaternions should produce unit quaternion\"\"\"\n        q = q / np.linalg.norm(q)  # Normalize\n        q2 = np.array([0.707, 0.707, 0, 0])  # 90° rotation\n        result = quaternion_multiply(q, q2)\n        assert abs(np.linalg.norm(result) - 1.0) < 1e-10\n```\n\n### Property-Based Testing with Hypothesis\n\n```python\nfrom hypothesis import given, strategies as st, settings\nimport hypothesis.extra.numpy as hnp\n\nclass TestTrajectoryInterpolation:\n\n    @given(\n        start=hnp.arrays(np.float64, (7,),\n            elements=st.floats(min_value=-3.14, max_value=3.14)),\n        end=hnp.arrays(np.float64, (7,),\n            elements=st.floats(min_value=-3.14, max_value=3.14)),\n        num_steps=st.integers(min_value=2, max_value=1000),\n    )\n    @settings(max_examples=200)\n    def test_interpolation_properties(self, start, end, num_steps):\n        \"\"\"Trajectory interpolation should satisfy mathematical properties\"\"\"\n        traj = linear_interpolate(start, end, num_steps)\n\n        # Property 1: Correct number of steps\n        assert len(traj) == num_steps\n\n        # Property 2: Starts at start, ends at end\n        np.testing.assert_allclose(traj[0], start, atol=1e-10)\n        np.testing.assert_allclose(traj[-1], end, atol=1e-10)\n\n        # Property 3: Monotonic progress (each step closer to goal)\n        for i in range(1, len(traj)):\n            dist_prev = np.linalg.norm(traj[i-1] - end)\n            dist_curr = np.linalg.norm(traj[i] - end)\n            assert dist_curr <= dist_prev + 1e-10\n\n        # Property 4: No jumps exceed max step size\n        diffs = np.diff(traj, axis=0)\n        max_step = np.max(np.abs(diffs))\n        expected_max = np.max(np.abs(end - start)) / (num_steps - 1)\n        assert max_step <= expected_max + 1e-10\n\n    @given(\n        points=hnp.arrays(np.float64, (3,),\n            elements=st.floats(min_value=-10, max_value=10, allow_nan=False)),\n    )\n    def test_transform_roundtrip(self, points):\n        \"\"\"Transform followed by inverse transform = identity\"\"\"\n        T = random_transform_matrix()\n        T_inv = np.linalg.inv(T)\n        transformed = transform_point(T, points)\n        recovered = transform_point(T_inv, transformed)\n        np.testing.assert_allclose(recovered, points, atol=1e-8)\n```\n\n## Integration Testing\n\n### ROS2 Launch Testing\n\n```python\n# test_integration.py\nimport pytest\nimport launch_testing\nfrom launch import LaunchDescription\nfrom launch_ros.actions import Node\nimport rclpy\nimport unittest\n\n@pytest.mark.launch_test\ndef generate_test_description():\n    \"\"\"Launch the nodes we want to test\"\"\"\n    perception_node = Node(\n        package='my_pkg', executable='perception_node',\n        parameters=[{'use_sim_time': True}],\n    )\n    planner_node = Node(\n        package='my_pkg', executable='planner_node',\n        parameters=[{'use_sim_time': True}],\n    )\n\n    return LaunchDescription([\n        perception_node,\n        planner_node,\n        launch_testing.actions.ReadyToTest(),\n    ])\n\n\nclass TestPerceptionPlannerIntegration(unittest.TestCase):\n\n    @classmethod\n    def setUpClass(cls):\n        rclpy.init()\n        cls.node = rclpy.create_node('integration_test')\n\n    @classmethod\n    def tearDownClass(cls):\n        cls.node.destroy_node()\n        rclpy.shutdown()\n\n    def test_perception_publishes_to_planner(self):\n        \"\"\"Perception detections should reach the planner\"\"\"\n        # Publish a test image\n        pub = self.node.create_publisher(Image, '/camera/image_raw', 10)\n        test_img = create_test_image_with_object()\n        pub.publish(test_img)\n\n        # Wait for planner output\n        received = []\n        sub = self.node.create_subscription(\n            Path, '/planner/path',\n            lambda msg: received.append(msg), 10)\n\n        end_time = self.node.get_clock().now() + rclpy.duration.Duration(seconds=5)\n        while self.node.get_clock().now() < end_time and not received:\n            rclpy.spin_once(self.node, timeout_sec=0.1)\n\n        self.assertGreater(len(received), 0, \"Planner should produce a path\")\n        self.assertGreater(len(received[0].poses), 0, \"Path should have poses\")\n```\n\n## Mock Hardware Patterns\n\n```python\nclass MockCamera:\n    \"\"\"Mock camera for testing without hardware\"\"\"\n\n    def __init__(self, image_dir=None, resolution=(640, 480)):\n        self.resolution = resolution\n        self.frame_count = 0\n\n        if image_dir:\n            # Use pre-recorded test images\n            self.images = self._load_test_images(image_dir)\n        else:\n            # Generate synthetic images\n            self.images = None\n\n    def get_frame(self):\n        self.frame_count += 1\n        if self.images:\n            idx = self.frame_count % len(self.images)\n            return self.images[idx]\n        else:\n            return self._generate_synthetic_frame()\n\n    def _generate_synthetic_frame(self):\n        \"\"\"Generate a deterministic test frame with known objects\"\"\"\n        img = np.zeros((*self.resolution[::-1], 3), dtype=np.uint8)\n        # Draw a red rectangle (simulated object)\n        img[100:200, 150:250] = [255, 0, 0]\n        return img\n\n\nclass MockJointStatePublisher:\n    \"\"\"Publish deterministic joint states for testing\"\"\"\n\n    def __init__(self, node, trajectory=None):\n        self.pub = node.create_publisher(\n            JointState, '/joint_states', 10)\n        self.step = 0\n\n        if trajectory is not None:\n            self.trajectory = trajectory\n        else:\n            # Sinusoidal motion for testing\n            t = np.linspace(0, 2*np.pi, 100)\n            self.trajectory = np.column_stack([\n                0.1 * np.sin(t + i * 0.5) for i in range(7)\n            ])\n\n    def publish_next(self):\n        msg = JointState()\n        msg.header.stamp = self.node.get_clock().now().to_msg()\n        msg.name = [f'joint_{i}' for i in range(7)]\n        idx = self.step % len(self.trajectory)\n        msg.position = self.trajectory[idx].tolist()\n        self.pub.publish(msg)\n        self.step += 1\n```\n\n## Golden File Testing (Trajectory Regression)\n\n```python\nclass TestTrajectoryRegression:\n    \"\"\"Compare planner output against known-good trajectories\"\"\"\n\n    GOLDEN_DIR = Path(__file__).parent / 'golden_trajectories'\n\n    def test_straight_line_plan(self):\n        start = np.array([0.3, 0.0, 0.5])\n        goal = np.array([0.5, 0.2, 0.3])\n\n        trajectory = planner.plan(start, goal)\n\n        golden_file = self.GOLDEN_DIR / 'straight_line.npy'\n        if not golden_file.exists():\n            # First run: save as golden\n            np.save(golden_file, trajectory)\n            pytest.skip(\"Golden file created — re-run to test\")\n\n        golden = np.load(golden_file)\n        np.testing.assert_allclose(trajectory, golden, atol=1e-4,\n            err_msg=\"Trajectory regression! Planner output changed.\")\n\n    def test_obstacle_avoidance_plan(self):\n        start = np.array([0.3, 0.0, 0.5])\n        goal = np.array([0.5, 0.2, 0.3])\n        obstacles = [Sphere(center=[0.4, 0.1, 0.4], radius=0.05)]\n\n        trajectory = planner.plan(start, goal, obstacles=obstacles)\n\n        # Verify no collisions\n        for point in trajectory:\n            for obs in obstacles:\n                dist = np.linalg.norm(point[:3] - obs.center)\n                assert dist > obs.radius, \\\n                    f\"Collision at {point[:3]}, dist={dist:.4f}\"\n```\n\n## Simulation Testing\n\n```python\nclass SimulationTestHarness:\n    \"\"\"Run behavior tests in simulation with deterministic physics\"\"\"\n\n    def __init__(self, sim_config):\n        self.sim = MuJoCoSimulator(sim_config)\n        self.sim.set_seed(42)  # Deterministic physics\n\n    def test_pick_and_place(self):\n        \"\"\"Full pick-and-place task in simulation\"\"\"\n        # Setup scene\n        self.sim.reset()\n        self.sim.spawn_object('red_block', pose=[0.4, 0.1, 0.02])\n\n        # Run behavior tree\n        bt = create_pick_place_tree()\n        bt.setup(sim=self.sim)\n\n        max_steps = 1000\n        for step in range(max_steps):\n            bt.tick()\n            self.sim.step()\n\n            if bt.root.status == Status.SUCCESS:\n                break\n\n        # Verify outcome\n        block_pose = self.sim.get_object_pose('red_block')\n        target_pose = np.array([0.5, -0.1, 0.02])\n        assert np.linalg.norm(block_pose[:3] - target_pose) < 0.02, \\\n            f\"Block not at target: {block_pose[:3]} vs {target_pose}\"\n        assert step < max_steps - 1, \"Task did not complete in time\"\n\n    def test_collision_safety(self):\n        \"\"\"Robot should never collide with table\"\"\"\n        self.sim.reset()\n        self.sim.spawn_object('obstacle', pose=[0.35, 0.0, 0.15])\n\n        trajectory = planner.plan_with_obstacle(\n            start=[0.3, -0.2, 0.3],\n            goal=[0.3, 0.2, 0.3])\n\n        for joints in trajectory:\n            self.sim.set_joint_positions(joints)\n            contacts = self.sim.get_contacts()\n            robot_contacts = [c for c in contacts\n                            if 'robot' in c.body1 or 'robot' in c.body2]\n            assert len(robot_contacts) == 0, \\\n                f\"Robot collision detected: {robot_contacts}\"\n```\n\n## CI/CD Pipeline for Robotics\n\n```yaml\n# .github/workflows/robotics_ci.yml\nname: Robotics CI\n\non: [push, pull_request]\n\njobs:\n  unit-tests:\n    runs-on: ubuntu-22.04\n    container:\n      image: ros:humble-ros-base\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install dependencies\n        run: |\n          apt-get update\n          rosdep install --from-paths src --ignore-src -y\n          pip install pytest hypothesis numpy\n\n      - name: Build\n        run: |\n          source /opt/ros/humble/setup.bash\n          colcon build --packages-select my_pkg\n          source install/setup.bash\n\n      - name: Unit tests\n        run: |\n          source install/setup.bash\n          colcon test --packages-select my_pkg\n          colcon test-result --verbose\n\n  integration-tests:\n    runs-on: ubuntu-22.04\n    container:\n      image: ros:humble-ros-base\n    needs: unit-tests\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Build full workspace\n        run: |\n          source /opt/ros/humble/setup.bash\n          colcon build\n\n      - name: Integration tests\n        run: |\n          source install/setup.bash\n          launch_test src/my_pkg/test/test_integration.py\n\n  simulation-tests:\n    runs-on: ubuntu-22.04\n    needs: integration-tests\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup MuJoCo\n        run: pip install mujoco\n\n      - name: Simulation tests\n        run: pytest tests/simulation/ -v --timeout=120\n```\n\n## Testing Anti-Patterns\n\n### 1. Testing with `sleep()`\n```python\n# BAD: Flaky, slow, non-deterministic\ndef test_message_received():\n    pub.publish(msg)\n    time.sleep(2.0)  # Hope it arrives!\n    assert received\n\n# GOOD: Event-driven waiting with timeout\ndef test_message_received():\n    pub.publish(msg)\n    event = threading.Event()\n    sub = create_sub(callback=lambda m: event.set())\n    assert event.wait(timeout=5.0), \"Message not received within timeout\"\n```\n\n### 2. Not Testing Failure Cases\n```python\n# BAD: Only test the happy path\n\n# GOOD: Test failures explicitly\ndef test_planner_unreachable_goal(self):\n    \"\"\"Planner should return None for unreachable goals\"\"\"\n    result = planner.plan(start, unreachable_goal)\n    assert result is None\n\ndef test_perception_no_objects(self):\n    \"\"\"Perception should return empty list when no objects visible\"\"\"\n    empty_image = np.zeros((256, 256, 3), dtype=np.uint8)\n    detections = perception.detect(empty_image)\n    assert detections == []\n```\n\n### 3. Non-Deterministic Tests\n```python\n# BAD: Random seed changes between runs\ntrajectory = planner.plan(start, goal)  # Uses random sampling internally\n\n# GOOD: Fix random seed for reproducibility\ndef test_rrt_planner(self):\n    np.random.seed(42)\n    trajectory = planner.plan(start, goal, seed=42)\n    assert len(trajectory) > 0\n```","tags":["robotics","testing","agent","skills","arpitg1304","agent-skills","ai-coding-assistant","claude-skills"],"capabilities":["skill","source-arpitg1304","skill-robotics-testing","topic-agent-skills","topic-ai-coding-assistant","topic-claude-skills","topic-robotics"],"categories":["robotics-agent-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/arpitg1304/robotics-agent-skills/robotics-testing","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add arpitg1304/robotics-agent-skills","source_repo":"https://github.com/arpitg1304/robotics-agent-skills","install_from":"skills.sh"}},"qualityScore":"0.544","qualityRationale":"deterministic score 0.54 from registry signals: · indexed on github topic:agent-skills · 189 github stars · SKILL.md body (18,076 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-02T18:54:21.137Z","embedding":null,"createdAt":"2026-04-18T22:05:37.574Z","updatedAt":"2026-05-02T18:54:21.137Z","lastSeenAt":"2026-05-02T18:54:21.137Z","tsv":"'-0.1':1548 '-0.2':1605 '-0.5':312,643,644,645 '-0.785':461 '-1':820,845,1208 '-10':901 '-2.356':463 '-22.04':1669,1743,1784 '-3.14':740,752 '/camera/image_raw':1058 '/joint_states':1246 '/opt/ros/humble/setup.bash':1708,1765 '/perception/detections':350 '/planner/path':1079 '0':249,317,387,460,462,464,656,657,658,695,696,813,871,1111,1120,1122,1152,1224,1225,1249,1264,1641,1977 '0.0':454,469,520,521,522,563,564,565,610,611,612,1346,1409,1597 '0.01':498 '0.02':1508,1549,1557 '0.05':1423 '0.088':453 '0.1':517,1107,1271,1420,1507 '0.15':1598 '0.2':560,1351,1414,1609 '0.3':561,1345,1352,1408,1415,1604,1606,1608,1610 '0.307':468 '0.35':1596 '0.4':516,1419,1421,1506 '0.5':518,559,603,604,605,606,635,636,637,638,642,1275,1347,1350,1410,1413,1547 '0.59':470 '0.7':280 '0.707':693,694 '0.785':466 '1':655,792,837,885,1178,1313,1573,1813 '1.0':375,382,519,562,609,708 '1.033':455 '1.571':465 '10':355,904,1059,1084,1247 '100':1219,1267 '1000':764,1522 '120':1808 '150':1221 '1e-10':623,660,709,816,823,858,891 '1e-4':542,1392 '1e-8':944 '2':761,803,1265,1868 '2.0':1831 '20':668 '200':768,1220 '250':1222 '255':250,1223 '256':239,241,245,251,252,1924,1925 '3':246,253,494,825,896,1209,1444,1453,1554,1565,1926,1935 '3.14':743,755 '30.0':287 '4':664,860 '42':1481,1967,1973 '480':1147 '4f':1456 '5':1092 '5.0':1862 '640':1146 '7':451,735,747,1280,1301 '90':697 'ab':705 'actions/checkout':1679,1757,1791 'actuat':71 'allclos':492,536,619,653,811,818,940,1388 'allow':366,905 'angl':445,477,490,524,534,553 'anti':1811 'anti-pattern':1810 'apt':1686 'apt-get':1685 'arriv':1834 'assert':274,281,314,384,578,704,797,853,886,1446,1550,1569,1637,1835,1859,1902,1933,1974 'atol':497,541,622,659,815,822,943,1391 'avoid':1403 'axi':870 'bad':1818,1874,1941 'base':77,712,1676,1750 'behavior':1463,1510 'block':1504,1537,1543,1552,1559,1563 'bottom':147,152 'break':1534 'briefli':364 'bt':1512 'bt.root.status':1532 'bt.setup':1517 'bt.tick':1529 'build':74,1705,1710,1760,1767 'c':1624,1626 'c.body1':1632 'c.body2':1636 'calibr':457,484 'callback':322,358,360,413,1855 'camera':1134 'case':1872 'center':1418 'chang':1399,1944 'cheap':154 'ci':1656 'ci/cd':73,93,1648 'class':259,441,591,729,1017,1131,1228,1320,1460 'classmethod':1020,1030 'clock':1088,1095,1289 'closer':830 'cls':1023,1033 'cls.node':1025 'cls.node.destroy':1034 'colcon':1709,1724,1731,1766 'collid':1588 'collis':1432,1450,1582,1644 'compar':1322 'complet':1577 'confid':277,309 'config':1474,1478 'contact':1619,1621,1623,1628,1640,1647 'contain':1670,1744 'context':196,199,212 'control':90,122 'correct':271,793 'count':1151,1177,1183 'crash':411 'creat':213,338,1062,1377,1513,1853 'curr':848,855 'data':408 'debug':98 'def':194,208,228,261,288,319,390,471,499,543,593,624,671,769,908,971,1021,1031,1037,1139,1172,1192,1236,1281,1337,1400,1470,1484,1580,1824,1844,1884,1906,1961 'default':272 'depend':421,1683 'descript':974 'detect':324,335,1045,1645,1929,1934 'detectionarray':349 'determinist':142,155,1199,1231,1468,1482,1823,1938 'diff':867,876 'dir':1143,1155,1165,1331,1360 'dist':840,847,854,856,1441,1447,1454,1455 'draw':1212 'driven':1840 'dtype':254,1210,1927 'element':736,748,897 'els':1166,1189,1257 'empti':392,404,414,1915,1921,1931 'empty/corrupted':401 'end':744,775,788,807,809,821,846,852,881,1085,1097 'enumer':575 'environ':113,123 'err':1393 'event':1839,1850 'event-driven':1838 'event.set':1858 'event.wait':1860 'exampl':767 'exceed':863 'execut':988,1002 'expect':446,478,495,877,889 'expens':158 'explicit':1883 'f':583,1294,1449,1558,1642 'failur':1871,1882 'fals':907 'fast':141,153 'fewer':148 'field':108 'file':1315,1333,1358,1372,1376,1386 'first':1365 'fix':1956 'fk':480,501,505 'flaki':99,1819 'follow':915 'forward':433,487,531 'frame':1174,1195,1201 'fresh':215 'from-path':1691 'full':126,1490,1761 'function':418 'function/class':140 'generat':231,972,1167,1193,1197 'get':1173,1687 'github/workflows/robotics_ci.yml':1653 'given':720,731,892 'goal':832,1348,1356,1411,1427,1607,1888,1896,1901,1950,1971 'golden':1314,1330,1335,1357,1369,1371,1375,1383,1385,1390 'golden_file.exists':1364 'good':1328,1837,1880,1955 'grace':403 'ground':84 'handl':394,400 'happi':1878 'hardwar':25,69,115,121,1128,1138 'hardware-in-the-loop':24,114 'hi':573,582,590 'hil':119 'hnp':728 'hnp.arrays':733,745,894 'home':448 'hope':1832 'humbl':1674,1748 'humble-ros-bas':1673,1747 'hypothesi':715,718,1702 'hypothesis.extra.numpy':726 'hz':285 'ident':595,599,607,617,632,919 'idx':1181,1188,1302,1308 'ignor':1696 'ignore-src':1695 'ik':502,506,549 'imag':181,230,235,237,321,329,332,357,362,393,402,406,1053,1057,1064,1142,1154,1161,1164,1169,1671,1745,1922,1932 'img':1061,1069,1205,1218,1227 'import':170,172,176,180,185,187,303,424,426,432,719,725,952,954,959,963,965,967 'init':1140,1237,1471 'initi':197,263,269 'instal':1682,1690,1700,1798 'install/setup.bash':1717,1723,1773 'integr':19,63,130,945,1028,1737,1769,1787 'integration-test':1736,1786 'intern':1954 'interpol':771,779,786 'inv':631,640,651,925,937 'invalid':298,313 'invers':435,525,567,626,917 'job':1661 'joint':444,476,489,523,533,545,555,566,576,580,584,586,1232,1295,1612,1616,1618 'jointstat':1245,1286 'jump':862 'kinemat':434,436,488,526,532,568 'known':456,473,483,1203,1327 'known-good':1326 'lambda':351,1080,1856 'launch':44,66,948,955,958,975,1774 'launch_ros.actions':962 'launch_testing.actions.readytotest':1016 'launchdescript':960,1011 'len':385,798,838,1109,1118,1184,1304,1638,1975 'limit':546,556,577 'line':1340 'linear':785 'list':1916 'lo':572,579,589 'loop':28,118 'm':1857 'match':482 'mathemat':782 'matrix':923 'max':741,753,762,766,864,872,878,887,890,902,1520,1527,1571 'mention':37 'messag':135,367,1826,1846,1863 'min':738,750,759,899 'mock':68,1127,1133 'mockcamera':1132 'mockjointstatepublish':1229 'modul':193,203 'monoton':826 'motion':1259 'msg':236,258,352,354,405,415,1081,1083,1285,1292,1311,1394,1829,1849 'msg.data':247 'msg.encoding':242 'msg.header.stamp':1287 'msg.height':238 'msg.name':1293 'msg.position':1306 'msg.step':244 'msg.width':240 'mujoco':1795,1799 'mujocosimul':1476 'multi':133 'multi-nod':132 'multipl':678 'multipli':438,596,615,627,648,701 'my_pkg.kinematics':431 'my_pkg.perception':183 'name':1654,1681,1704,1718,1759,1768,1793,1800 'nan':906 'need':1751,1785 'never':1587 'next':1283 'node':40,60,134,165,177,184,210,217,221,224,226,266,267,294,295,327,343,344,372,379,389,397,398,964,977,983,984,990,997,998,1004,1013,1015,1027,1035,1239 'node.create':1243 'node.destroy':225 'non':1822,1937 'non-determinist':1821,1936 'none':1144,1171,1241,1254,1893,1905 'normal':690 'np':190,429 'np.abs':875,880 'np.array':452,459,467,515,558,602,608,634,641,692,1344,1349,1407,1412,1546 'np.column':1269 'np.diff':868 'np.float64':734,746,895 'np.linalg.inv':926 'np.linalg.norm':688,706,842,849,1442,1551 'np.linspace':1263 'np.load':1384 'np.max':874,879 'np.pi':1266 'np.random.randint':248 'np.random.randn':663 'np.random.seed':1966 'np.save':1370 'np.sin':1272 'np.testing.assert':491,535,618,652,810,817,939,1387 'np.uint8':255,1211,1928 'np.zeros':450,1206,1923 'num':756,776,789,800,883 'number':794 'numpi':188,427,1703 'ob':1438 'object':1066,1204,1217,1502,1540,1593,1910,1919 'obs.center':1445 'obs.radius':1448 'obstacl':1402,1416,1428,1429,1440,1594,1602 'origin':511,513,527,539 'outcom':1536 'output':336,1073,1324,1398 'outsid':554,588 'packag':97,985,999,1712,1727 'packages-select':1711,1726 'paramet':273,276,283,290,299,307,991,1005 'parent':1334 'pass':136 'path':1078,1116,1123,1332,1693,1879 'pattern':6,162,1129,1812 'per':201 'percept':81,209,216,265,293,326,378,396,982,989,1012,1039,1044,1908,1912 'perception.detect':1930 'perception_node.get':275,282 'perception_node.image':359,412 'perception_node.set':306 'perceptionnod':186,222 'physic':129,1469,1483 'pick':1486,1492,1514 'pick-and-plac':1491 'pip':1699,1797 'pipelin':82,94,1649 'pkg':987,1001,1715,1730 'place':1488,1494,1515 'plan':1341,1404 'planner':88,996,1003,1014,1042,1049,1072,1112,1323,1397,1886,1890,1964 'planner.plan':1354,1425,1600,1898,1948,1969 'point':440,893,913,930,932,935,942,1434,1443,1452 'pos':447,479,496 'pose':458,474,507,512,514,528,530,538,540,1121,1126,1505,1538,1541,1545,1553,1556,1564,1568,1595 'posit':449,485,1617 'pre':1158 'pre-record':1157 'preserv':675 'prev':841,857 'process':330 'produc':334,683,1114 'progress':827 'propag':368 'properti':711,772,783,791,802,824,859 'property-bas':710 'pub':1054 'pub.publish':1067,1828,1848 'publish':323,1040,1050,1056,1230,1244,1282 'pull':1659 'pure':417 'push':1658 'pyramid':107 'pytest':41,167,171,425,953,1701,1804 'pytest.fixture':191,207,227 'pytest.mark.launch':969 'pytest.mark.parametrize':443,661 'pytest.skip':1374 'python':168,422,716,950,1130,1319,1459,1817,1873,1940 'q':598,600,601,616,621,629,630,633,639,649,650,662,677,686,687,689,702 'q2':691,703 'quaternion':437,614,647,670,674,681,685,700 'radius':1422 'random':669,921,1942,1952,1957 'rang':667,836,1279,1300,1526 'rate':284 'rcl_interfaces.msg':302 'rclpi':173,966 'rclpy.create':1026 'rclpy.duration.duration':1090 'rclpy.init':204,1024 'rclpy.node':175 'rclpy.parameter.parameter':308 'rclpy.shutdown':206,1036 'rclpy.spin':369,376,1102 're':1379 're-run':1378 'reach':1047 'real':110,112,120 'realist':128,159 'receiv':337,386,1074,1101,1110,1119,1827,1836,1847,1865 'received.append':353,1082 'record':1159 'recov':529,537,933,941 'rectangl':1215 'red':1214,1503,1542 'regress':1318,1396 'reject':297 'reproduc':1960 'request':1660 'resolut':1145,1149 'respect':547 'result':305,316,486,493,613,620,646,654,699,707,1734,1897,1903 'return':257,509,552,1010,1186,1190,1226,1892,1914 'rgb8':243 'robot':2,10,31,47,96,102,105,111,1585,1622,1630,1634,1639,1643,1646,1651,1655 'robotics-test':1 'ros':39,43,195,211,420,1672,1675,1746,1749 'ros1/ros2':59 'ros2':164,198,947 'rosdep':1689 'rotat':698 'roundtrip':503,911 'rrt':1963 'run':1366,1380,1462,1509,1666,1684,1706,1721,1740,1763,1771,1781,1796,1803,1946 'runs-on':1665,1739,1780 'safeti':1583 'sampl':1953 'satisfi':781 'save':1367 'scene':1499 'scope':192 'sec':374,381,1106 'second':1091 'seed':1480,1943,1958,1972 'select':1713,1728 'self':264,292,325,395,475,504,548,597,628,676,773,912,1043,1141,1175,1196,1238,1284,1342,1405,1472,1489,1584,1889,1911,1965 'self._generate_synthetic_frame':1191 'self._load_test_images':1163 'self.assertgreater':1108,1117 'self.frame':1150,1176,1182 'self.golden':1359 'self.images':1162,1170,1180,1185,1187 'self.node':1104 'self.node.create':1055,1076 'self.node.get':1087,1094,1288 'self.pub':1242 'self.pub.publish':1310 'self.resolution':1148,1207 'self.sim':1475,1519 'self.sim.get':1539,1620 'self.sim.reset':1500,1591 'self.sim.set':1479,1615 'self.sim.spawn':1501,1592 'self.sim.step':1530 'self.step':1248,1303,1312 'self.trajectory':1255,1268,1305,1307 'sensor':70 'sensor_msgs.msg':179 'set':61,91,724,765 'setparametersresult':304 'setup':1498,1794 'setupclass':1022 'sim':127,993,1007,1473,1477,1518 'simu':46 'simul':21,76,124,356,1216,1457,1466,1497,1778,1801 'simulation-bas':75 'simulation-test':1777 'simulationtesthar':1461 'singl':139 'sinusoid':1258 'size':866 'skill':14,49,54 'skill-robotics-testing' 'sleep':1816 'slow':157,1820 'softwar':11 'sourc':1707,1716,1722,1764,1772 'source-arpitg1304' 'sphere':1417 'spin':363 'src':1694,1697 'src/my_pkg/test/test_integration.py':1776 'st':723 'st.floats':737,749,898 'st.integers':758 'stack':1270 'start':732,774,787,804,806,814,882,1343,1355,1406,1426,1603,1899,1949,1970 'state':1233 'status.success':1533 'step':757,777,790,796,801,829,865,873,884,888,1521,1524,1528,1570,1572,1677,1755,1789 'straight':1339 'straight_line.npy':1361 'strategi':5,721 'sub':342,371,1075,1852,1854 'sub_node.create':347 'sub_node.destroy':388 'subscrib':341,346 'subscript':348,1077 'success':318 'suit':79 'synthet':233,1168,1194 'system':32,103 'tabl':1590 'target':557,569,1544,1555,1562,1567 'task':1495,1574 'teardownclass':1032 'test':3,4,18,20,22,29,38,45,48,57,64,67,78,80,100,106,109,125,131,138,144,161,163,202,220,229,234,262,289,320,328,340,345,361,391,416,472,500,544,594,625,672,713,770,909,946,949,956,970,973,981,1029,1038,1052,1060,1063,1068,1136,1160,1200,1235,1261,1316,1338,1382,1401,1458,1464,1485,1581,1664,1720,1725,1733,1738,1754,1770,1775,1779,1788,1802,1809,1814,1825,1845,1870,1876,1881,1885,1907,1939,1962 'test-result':1732 'test_integration.py':951 'test_kinematics.py':423 'test_perception_node.py':169 'testforwardkinemat':442 'testperceptionnod':260 'testperceptionplannerintegr':1018 'testquaternionmath':592 'tests/simulation':1805 'testtrajectoryinterpol':730 'testtrajectoryregress':1321 'threading.event':1851 'threshold':278,310 'time':994,1008,1086,1098,1579 'time.sleep':1830 'timeout':373,380,1105,1807,1843,1861,1867 'tobyt':256 'tolist':1309 'tool':8 'top':151,156 'topic-agent-skills' 'topic-ai-coding-assistant' 'topic-claude-skills' 'topic-robotics' 'traj':784,799,812,819,839,843,850,869 'trajectori':87,778,1240,1251,1256,1317,1329,1336,1353,1373,1389,1395,1424,1436,1599,1614,1947,1968,1976 'transform':439,910,914,918,922,928,929,934,938 'tree':1511,1516 'trigger':33 'true':995,1009 'truth':85 'ubuntu':1668,1742,1783 'unit':17,56,137,160,673,680,684,1663,1719,1753 'unit-test':1662,1752 'unittest':968 'unittest.testcase':1019 'unreach':1887,1895,1900 'updat':1688 'use':12,52,992,1006,1156,1678,1756,1790,1951 'user':36 'v':1806 'v4':1680,1758,1792 'valid':86,291 'valu':279,286,300,311,739,742,751,754,760,763,900,903 'verbos':1735 'verifi':383,1430,1535 'visibl':1920 'vs':1566 'wait':1070,1841 'want':979 'whenev':34 'within':1866 'without':1137 'workspac':1762 'write':16,55 'y':1698 'yaml':1652 'yield':205,223","prices":[{"id":"af0bb05a-ef4a-4857-80fd-e79e9bf74ca1","listingId":"53298d44-6a5f-480e-8d4f-472f47e87334","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"arpitg1304","category":"robotics-agent-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T22:05:37.574Z"}],"sources":[{"listingId":"53298d44-6a5f-480e-8d4f-472f47e87334","source":"github","sourceId":"arpitg1304/robotics-agent-skills/robotics-testing","sourceUrl":"https://github.com/arpitg1304/robotics-agent-skills/tree/main/skills/robotics-testing","isPrimary":false,"firstSeenAt":"2026-04-18T22:05:37.574Z","lastSeenAt":"2026-05-02T18:54:21.137Z"}],"details":{"listingId":"53298d44-6a5f-480e-8d4f-472f47e87334","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"arpitg1304","slug":"robotics-testing","github":{"repo":"arpitg1304/robotics-agent-skills","stars":189,"topics":["agent-skills","ai-coding-assistant","claude-skills","robotics"],"license":"apache-2.0","html_url":"https://github.com/arpitg1304/robotics-agent-skills","pushed_at":"2026-03-25T03:44:12Z","description":"Agent skills that make AI coding assistants write production-grade robotics software. ROS1, ROS2, design patterns, SOLID principles, and testing — for Claude Code, Cursor, Copilot, and any SKILL.md-compatible agent.","skill_md_sha":"c2c2c3326591d49935381ffc43a9342b88b53e55","skill_md_path":"skills/robotics-testing/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/arpitg1304/robotics-agent-skills/tree/main/skills/robotics-testing"},"layout":"multi","source":"github","category":"robotics-agent-skills","frontmatter":{"name":"robotics-testing","description":"Testing strategies, patterns, and tools for robotics software. Use this skill when writing unit tests, integration tests, simulation tests, or hardware-in-the-loop tests for robot systems. Trigger whenever the user mentions testing ROS nodes, pytest with ROS, launch_testing, simulation testing, CI/CD for robotics, test fixtures for sensors, mock hardware, deterministic replay, regression testing for robot behaviors, or validating perception/planning/control pipelines. Also covers property-based testing for kinematics, fuzz testing for message handlers, and golden-file testing for trajectories."},"skills_sh_url":"https://skills.sh/arpitg1304/robotics-agent-skills/robotics-testing"},"updatedAt":"2026-05-02T18:54:21.137Z"}}