Imagine you’re building a complex robot using ROS 2, the latest version of the Robot Operating System. Your robot is made up of many different parts, each controlled by a piece of software. Now, how do you make sure all these pieces work correctly? That’s where unit testing comes in.
Unit testing is like doing a health check-up for each part of your robot’s software. It’s a way to automatically test small pieces of your code to make sure they’re doing exactly what they’re supposed to do.
Unit tests are essential tools in robotics software development for two key reasons:
- Early Bug Detection: Automate the testing of code to ensure that changes or updates don’t introduce bugs.
- Improved Code Design: Writing testable code leads to cleaner, more modular software.
In this tutorial, I’ll show you how to use a tool called Pytest to write and run these unit tests. With Pytest, you can write tests for individual parts of your code, such as functions and classes, to make sure each part behaves as expected.
Pytest is designed primarily for Python code, so it’s commonly used for testing Python-based ROS 2 packages. For C++ code in ROS 2, other testing frameworks, like GTest, are typically used instead.
Prerequisites
- You have completed this tutorial: How to Create a ROS 2 Python Publisher – Jazzy (required)
- You have completed this tutorial: How to Create a Subscriber Node in C++ – Jazzy (recommended)
- I am assuming you are using Visual Studio Code, but you can use any code editor.
You can find all the code here on GitHub.
Create a Unit Test
Let’s write a basic unit test for our ROS 2 publisher node. This test will verify three key things:
- that our node is created correctly
- that its message counter works properly
- that it formats messages as expected.
We’ll create a Python test file that uses Pytest to run these checks automatically.
Open a terminal window, and move to your workspace.
cd ~/ros2_ws/
colcon build && source ~/.bashrc
cd ~/ros2_ws/src/ros2_fundamentals_examples/
mkdir -p test/pytest
Create a test file test/pytest/test_publisher.py:
#!/usr/bin/env python3
"""
Test suite for the ROS2 minimal publisher node.
This script contains unit tests for verifying the functionality of a minimal ROS2 publisher.
It tests the node creation, message counter increment, and message content formatting.
Subscription Topics:
None
Publishing Topics:
/py_example_topic (std_msgs/String): Example messages with incrementing counter
:author: Addison Sears-Collins
:date: November 6, 2024
"""
import pytest
import rclpy
from std_msgs.msg import String
from ros2_fundamentals_examples.py_minimal_publisher import MinimalPyPublisher
def test_publisher_creation():
"""
Test if the publisher node is created correctly.
This test verifies:
1. The node name is set correctly
2. The publisher object exists
3. The topic name is correct
:raises: AssertionError if any of the checks fail
"""
# Initialize ROS2 communication
rclpy.init()
try:
# Create an instance of our publisher node
node = MinimalPyPublisher()
# Test 1: Verify the node has the expected name
assert node.get_name() == 'minimal_py_publisher'
# Test 2: Verify the publisher exists and has the correct topic name
assert hasattr(node, 'publisher_1')
assert node.publisher_1.topic_name == '/py_example_topic'
finally:
# Clean up ROS2 communication
rclpy.shutdown()
def test_message_counter():
"""
Test if the message counter increments correctly.
This test verifies that the counter (node.i) increases by 1 after
each timer callback execution.
:raises: AssertionError if the counter doesn't increment properly
"""
rclpy.init()
try:
node = MinimalPyPublisher()
initial_count = node.i
node.timer_callback()
assert node.i == initial_count + 1
finally:
rclpy.shutdown()
def test_message_content():
"""
Test if the message content is formatted correctly.
This test verifies that the message string is properly formatted
using an f-string with the current counter value.
:raises: AssertionError if the message format doesn't match expected output
"""
rclpy.init()
try:
node = MinimalPyPublisher()
# Set counter to a known value for testing
node.i = 5
msg = String()
# Using f-string instead of % formatting
msg.data = f'Hello World: {node.i}'
assert msg.data == 'Hello World: 5'
finally:
rclpy.shutdown()
if __name__ == '__main__':
pytest.main(['-v'])
Edit CMakeLists.txt
Now open CMakeLists.txt, and replace the if(BUILD_TESTING) block with this code:
if(BUILD_TESTING)
#find_package(ament_lint_auto REQUIRED)
find_package(ament_cmake_pytest REQUIRED)
#set(ament_cmake_copyright_FOUND TRUE)
#set(ament_cmake_cpplint_FOUND TRUE)
#ament_lint_auto_find_test_dependencies()
ament_add_pytest_test(minimal_publisher_test test/pytest/test_publisher.py
TIMEOUT 60
)
endif()
Edit the package.xml File
We need to add this to the package.xml so that our tests are discoverable to cmake:
<test_depend>ament_cmake_pytest</test_depend>
Save the file, and close it.
Build and Run
Compile and run the tests.
cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-select ros2_fundamentals_examples
source ~/.bashrc
colcon test --packages-select ros2_fundamentals_examples
To get more detail, you could also have done:
colcon test --packages-select ros2_fundamentals_examples --event-handlers console_direct+
When we run our unit tests with this command above (Starting >>> ros2_fundamentals_examples), it first starts by loading our ROS 2 package.
The system then collects and runs our three test cases, shown as the three passing tests in the output (test/pytest/test_publisher.py …). These dots represent successful runs of our three tests – publisher node creation, message counter, and message content.
Your test results should show all tests passed successfully (============================== 3 passed in 0.28s ===============================).
My final summary shows that our single test suite (1/1 Test #1: minimal_publisher_test) passed successfully in 0.76 seconds, with CMake reporting 100% success rate as no tests failed.
Now once that is finished, check the results:
colcon test-result --all
To see which test cases failed, type:
colcon test-result --all --verbose
That’s it.
Keep building!