<< All versions
1. Testing with
Skill v1.0.1
currentLLM-judged scan95/100arpitg1304/robotics-agent-skills/robotics-testing
1 files
──Details
PublishedJune 27, 2026 at 12:41 AM
Content Hashsha256:2de46ea2c1d831c7...
Git SHA54f7b578f3dc
Bump Typepatch
──Files
Files (1 file, 18.5 KB)
SKILL.md18.5 KBactive
SKILL.md · 579 lines · 18.5 KB
version: "1.0.1" 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.
Robotics Testing Skill
When to Use This Skill
- Writing unit tests for ROS1/ROS2 nodes
- Setting up integration tests with launch_testing
- Mocking hardware (sensors, actuators) for CI/CD
- Building simulation-based test suites
- Testing perception pipelines with ground truth
- Validating trajectory planners and controllers
- Setting up CI/CD pipelines for robotics packages
- Debugging flaky tests in robotics systems
The Robotics Testing Pyramid
╱╲╱ ╲ Field Tests╱ ╲ (Real robot, real environment)╱──────╲╱ ╲ Hardware-in-the-Loop (HIL)╱ ╲ (Real hardware, controlled environment)╱────────────╲╱ ╲ Simulation Tests╱ ╲ (Full sim, realistic physics)╱──────────────────╲╱ ╲ Integration Tests╱ ╲ (Multi-node, message passing)╱────────────────────────╲╱ ╲ Unit Tests╱____________________________╲ (Single function/class, fast, deterministic)MORE tests at the bottom, FEWER at the top.Bottom = fast, cheap, deterministic. Top = slow, expensive, realistic.
Unit Testing Patterns
Testing ROS2 Nodes with pytest
python
# test_perception_node.pyimport pytestimport rclpyfrom rclpy.node import Nodefrom sensor_msgs.msg import Imagefrom my_pkg.perception_node import PerceptionNodeimport numpy as np@pytest.fixture(scope='module')def ros_context():"""Initialize ROS2 context once per test module"""rclpy.init()yieldrclpy.shutdown()@pytest.fixturedef perception_node(ros_context):"""Create a fresh perception node for each test"""node = PerceptionNode()yield nodenode.destroy_node()@pytest.fixturedef test_image():"""Generate a synthetic test image"""msg = Image()msg.height = 256msg.width = 256msg.encoding = 'rgb8'msg.step = 256 * 3msg.data = np.random.randint(0, 255, (256, 256, 3),dtype=np.uint8).tobytes()return msgclass TestPerceptionNode:def test_initialization(self, perception_node):"""Node should initialize with correct default parameters"""assert perception_node.get_parameter('confidence_threshold').value == 0.7assert perception_node.get_parameter('rate_hz').value == 30.0def test_parameter_validation(self, perception_node):"""Node should reject invalid parameter values"""from rcl_interfaces.msg import SetParametersResultresult = perception_node.set_parameters([rclpy.parameter.Parameter('confidence_threshold',value=-0.5) # Invalid!])assert not result[0].successfuldef test_image_callback_publishes_detections(self, perception_node, test_image):"""Processing an image should produce detection output"""received = []# Create a test subscribersub_node = Node('test_subscriber')sub_node.create_subscription(DetectionArray, '/perception/detections',lambda msg: received.append(msg), 10)# Simulate image callbackperception_node.image_callback(test_image)# Spin briefly to allow message propagationrclpy.spin_once(sub_node, timeout_sec=1.0)rclpy.spin_once(perception_node, timeout_sec=1.0)# Verifyassert len(received) > 0sub_node.destroy_node()def test_empty_image_handling(self, perception_node):"""Node should handle empty/corrupted images gracefully"""empty_msg = Image() # No data# Should not crashperception_node.image_callback(empty_msg)
Testing Pure Functions (No ROS Dependency)
python
# test_kinematics.pyimport pytestimport numpy as npfrom my_pkg.kinematics import (forward_kinematics, inverse_kinematics,quaternion_multiply, transform_point)class TestForwardKinematics:@pytest.mark.parametrize("joint_angles,expected_pos", [# Home position(np.zeros(7), np.array([0.088, 0.0, 1.033])),# Known calibrated pose(np.array([0, -0.785, 0, -2.356, 0, 1.571, 0.785]),np.array([0.307, 0.0, 0.59])),])def test_known_poses(self, joint_angles, expected_pos):"""FK should match known calibrated positions"""result = forward_kinematics(joint_angles)np.testing.assert_allclose(result[:3], expected_pos, atol=0.01)def test_fk_ik_roundtrip(self):"""FK(IK(pose)) should return the original pose"""original_pose = np.array([0.4, 0.1, 0.5, 1.0, 0.0, 0.0, 0.0])joint_angles = inverse_kinematics(original_pose)recovered_pose = forward_kinematics(joint_angles)np.testing.assert_allclose(recovered_pose, original_pose, atol=1e-4)def test_joint_limits_respected(self):"""IK should not return angles outside joint limits"""target = np.array([0.5, 0.2, 0.3, 1.0, 0.0, 0.0, 0.0])joints = inverse_kinematics(target)for i, (lo, hi) in enumerate(JOINT_LIMITS):assert lo <= joints[i] <= hi, \f"Joint {i}: {joints[i]} outside [{lo}, {hi}]"class TestQuaternionMath:def test_identity_multiply(self):"""q * identity = q"""q = np.array([0.5, 0.5, 0.5, 0.5])identity = np.array([1.0, 0.0, 0.0, 0.0])result = quaternion_multiply(q, identity)np.testing.assert_allclose(result, q, atol=1e-10)def test_inverse_multiply(self):"""q * q_inv = identity"""q = np.array([0.5, 0.5, 0.5, 0.5])q_inv = np.array([0.5, -0.5, -0.5, -0.5])result = quaternion_multiply(q, q_inv)np.testing.assert_allclose(result, [1, 0, 0, 0], atol=1e-10)@pytest.mark.parametrize("q", [np.random.randn(4) for _ in range(20) # Random quaternions])def test_unit_quaternion_preserved(self, q):"""Multiplication of unit quaternions should produce unit quaternion"""q = q / np.linalg.norm(q) # Normalizeq2 = np.array([0.707, 0.707, 0, 0]) # 90° rotationresult = quaternion_multiply(q, q2)assert abs(np.linalg.norm(result) - 1.0) < 1e-10
Property-Based Testing with Hypothesis
python
from hypothesis import given, strategies as st, settingsimport hypothesis.extra.numpy as hnpclass TestTrajectoryInterpolation:@given(start=hnp.arrays(np.float64, (7,),elements=st.floats(min_value=-3.14, max_value=3.14)),end=hnp.arrays(np.float64, (7,),elements=st.floats(min_value=-3.14, max_value=3.14)),num_steps=st.integers(min_value=2, max_value=1000),)@settings(max_examples=200)def test_interpolation_properties(self, start, end, num_steps):"""Trajectory interpolation should satisfy mathematical properties"""traj = linear_interpolate(start, end, num_steps)# Property 1: Correct number of stepsassert len(traj) == num_steps# Property 2: Starts at start, ends at endnp.testing.assert_allclose(traj[0], start, atol=1e-10)np.testing.assert_allclose(traj[-1], end, atol=1e-10)# Property 3: Monotonic progress (each step closer to goal)for i in range(1, len(traj)):dist_prev = np.linalg.norm(traj[i-1] - end)dist_curr = np.linalg.norm(traj[i] - end)assert dist_curr <= dist_prev + 1e-10# Property 4: No jumps exceed max step sizediffs = np.diff(traj, axis=0)max_step = np.max(np.abs(diffs))expected_max = np.max(np.abs(end - start)) / (num_steps - 1)assert max_step <= expected_max + 1e-10@given(points=hnp.arrays(np.float64, (3,),elements=st.floats(min_value=-10, max_value=10, allow_nan=False)),)def test_transform_roundtrip(self, points):"""Transform followed by inverse transform = identity"""T = random_transform_matrix()T_inv = np.linalg.inv(T)transformed = transform_point(T, points)recovered = transform_point(T_inv, transformed)np.testing.assert_allclose(recovered, points, atol=1e-8)
Integration Testing
ROS2 Launch Testing
python
# test_integration.pyimport pytestimport launch_testingfrom launch import LaunchDescriptionfrom launch_ros.actions import Nodeimport rclpyimport unittest@pytest.mark.launch_testdef generate_test_description():"""Launch the nodes we want to test"""perception_node = Node(package='my_pkg', executable='perception_node',parameters=[{'use_sim_time': True}],)planner_node = Node(package='my_pkg', executable='planner_node',parameters=[{'use_sim_time': True}],)return LaunchDescription([perception_node,planner_node,launch_testing.actions.ReadyToTest(),])class TestPerceptionPlannerIntegration(unittest.TestCase):@classmethoddef setUpClass(cls):rclpy.init()cls.node = rclpy.create_node('integration_test')@classmethoddef tearDownClass(cls):cls.node.destroy_node()rclpy.shutdown()def test_perception_publishes_to_planner(self):"""Perception detections should reach the planner"""# Publish a test imagepub = self.node.create_publisher(Image, '/camera/image_raw', 10)test_img = create_test_image_with_object()pub.publish(test_img)# Wait for planner outputreceived = []sub = self.node.create_subscription(Path, '/planner/path',lambda msg: received.append(msg), 10)end_time = self.node.get_clock().now() + rclpy.duration.Duration(seconds=5)while self.node.get_clock().now() < end_time and not received:rclpy.spin_once(self.node, timeout_sec=0.1)self.assertGreater(len(received), 0, "Planner should produce a path")self.assertGreater(len(received[0].poses), 0, "Path should have poses")
Mock Hardware Patterns
python
class MockCamera:"""Mock camera for testing without hardware"""def __init__(self, image_dir=None, resolution=(640, 480)):self.resolution = resolutionself.frame_count = 0if image_dir:# Use pre-recorded test imagesself.images = self._load_test_images(image_dir)else:# Generate synthetic imagesself.images = Nonedef get_frame(self):self.frame_count += 1if self.images:idx = self.frame_count % len(self.images)return self.images[idx]else:return self._generate_synthetic_frame()def _generate_synthetic_frame(self):"""Generate a deterministic test frame with known objects"""img = np.zeros((*self.resolution[::-1], 3), dtype=np.uint8)# Draw a red rectangle (simulated object)img[100:200, 150:250] = [255, 0, 0]return imgclass MockJointStatePublisher:"""Publish deterministic joint states for testing"""def __init__(self, node, trajectory=None):self.pub = node.create_publisher(JointState, '/joint_states', 10)self.step = 0if trajectory is not None:self.trajectory = trajectoryelse:# Sinusoidal motion for testingt = np.linspace(0, 2*np.pi, 100)self.trajectory = np.column_stack([0.1 * np.sin(t + i * 0.5) for i in range(7)])def publish_next(self):msg = JointState()msg.header.stamp = self.node.get_clock().now().to_msg()msg.name = [f'joint_{i}' for i in range(7)]idx = self.step % len(self.trajectory)msg.position = self.trajectory[idx].tolist()self.pub.publish(msg)self.step += 1
Golden File Testing (Trajectory Regression)
python
class TestTrajectoryRegression:"""Compare planner output against known-good trajectories"""GOLDEN_DIR = Path(__file__).parent / 'golden_trajectories'def test_straight_line_plan(self):start = np.array([0.3, 0.0, 0.5])goal = np.array([0.5, 0.2, 0.3])trajectory = planner.plan(start, goal)golden_file = self.GOLDEN_DIR / 'straight_line.npy'if not golden_file.exists():# First run: save as goldennp.save(golden_file, trajectory)pytest.skip("Golden file created — re-run to test")golden = np.load(golden_file)np.testing.assert_allclose(trajectory, golden, atol=1e-4,err_msg="Trajectory regression! Planner output changed.")def test_obstacle_avoidance_plan(self):start = np.array([0.3, 0.0, 0.5])goal = np.array([0.5, 0.2, 0.3])obstacles = [Sphere(center=[0.4, 0.1, 0.4], radius=0.05)]trajectory = planner.plan(start, goal, obstacles=obstacles)# Verify no collisionsfor point in trajectory:for obs in obstacles:dist = np.linalg.norm(point[:3] - obs.center)assert dist > obs.radius, \f"Collision at {point[:3]}, dist={dist:.4f}"
Simulation Testing
python
class SimulationTestHarness:"""Run behavior tests in simulation with deterministic physics"""def __init__(self, sim_config):self.sim = MuJoCoSimulator(sim_config)self.sim.set_seed(42) # Deterministic physicsdef test_pick_and_place(self):"""Full pick-and-place task in simulation"""# Setup sceneself.sim.reset()self.sim.spawn_object('red_block', pose=[0.4, 0.1, 0.02])# Run behavior treebt = create_pick_place_tree()bt.setup(sim=self.sim)max_steps = 1000for step in range(max_steps):bt.tick()self.sim.step()if bt.root.status == Status.SUCCESS:break# Verify outcomeblock_pose = self.sim.get_object_pose('red_block')target_pose = np.array([0.5, -0.1, 0.02])assert np.linalg.norm(block_pose[:3] - target_pose) < 0.02, \f"Block not at target: {block_pose[:3]} vs {target_pose}"assert step < max_steps - 1, "Task did not complete in time"def test_collision_safety(self):"""Robot should never collide with table"""self.sim.reset()self.sim.spawn_object('obstacle', pose=[0.35, 0.0, 0.15])trajectory = planner.plan_with_obstacle(start=[0.3, -0.2, 0.3],goal=[0.3, 0.2, 0.3])for joints in trajectory:self.sim.set_joint_positions(joints)contacts = self.sim.get_contacts()robot_contacts = [c for c in contactsif 'robot' in c.body1 or 'robot' in c.body2]assert len(robot_contacts) == 0, \f"Robot collision detected: {robot_contacts}"
CI/CD Pipeline for Robotics
yaml
# .github/workflows/robotics_ci.ymlname: Robotics CIon: [push, pull_request]jobs:unit-tests:runs-on: ubuntu-22.04container:image: ros:humble-ros-basesteps:- uses: actions/checkout@v4- name: Install dependenciesrun: |apt-get updaterosdep install --from-paths src --ignore-src -ypip install pytest hypothesis numpy- name: Buildrun: |source /opt/ros/humble/setup.bashcolcon build --packages-select my_pkgsource install/setup.bash- name: Unit testsrun: |source install/setup.bashcolcon test --packages-select my_pkgcolcon test-result --verboseintegration-tests:runs-on: ubuntu-22.04container:image: ros:humble-ros-baseneeds: unit-testssteps:- uses: actions/checkout@v4- name: Build full workspacerun: |source /opt/ros/humble/setup.bashcolcon build- name: Integration testsrun: |source install/setup.bashlaunch_test src/my_pkg/test/test_integration.pysimulation-tests:runs-on: ubuntu-22.04needs: integration-testssteps:- uses: actions/checkout@v4- name: Setup MuJoCorun: pip install mujoco- name: Simulation testsrun: pytest tests/simulation/ -v --timeout=120
Testing Anti-Patterns
1. Testing with sleep()
python
# BAD: Flaky, slow, non-deterministicdef test_message_received():pub.publish(msg)time.sleep(2.0) # Hope it arrives!assert received# GOOD: Event-driven waiting with timeoutdef test_message_received():pub.publish(msg)event = threading.Event()sub = create_sub(callback=lambda m: event.set())assert event.wait(timeout=5.0), "Message not received within timeout"
2. Not Testing Failure Cases
python
# BAD: Only test the happy path# GOOD: Test failures explicitlydef test_planner_unreachable_goal(self):"""Planner should return None for unreachable goals"""result = planner.plan(start, unreachable_goal)assert result is Nonedef test_perception_no_objects(self):"""Perception should return empty list when no objects visible"""empty_image = np.zeros((256, 256, 3), dtype=np.uint8)detections = perception.detect(empty_image)assert detections == []
3. Non-Deterministic Tests
python
# BAD: Random seed changes between runstrajectory = planner.plan(start, goal) # Uses random sampling internally# GOOD: Fix random seed for reproducibilitydef test_rrt_planner(self):np.random.seed(42)trajectory = planner.plan(start, goal, seed=42)assert len(trajectory) > 0