Skip to content

Commit 3d4b2ef

Browse files
abaeyensclalancettefujitatomoyakscottz
authored
Add tutorial on integration testing (#4881)
* tutorial/integration_testing: first draft Signed-off-by: Arne Baeyens <[email protected]> Co-authored-by: Chris Lalancette <[email protected]> Co-authored-by: Tomoya Fujita <[email protected]> Co-authored-by: Katherine Scott <[email protected]>
1 parent 7f9540c commit 3d4b2ef

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
Writing Basic Integration Tests with launch_testing
2+
===================================================
3+
4+
**Goal:** Create and run integration tests on the ROS 2 turtlesim node.
5+
6+
**Tutorial level:** Intermediate
7+
8+
**Time:** 20 minutes
9+
10+
.. contents:: Contents
11+
:depth: 2
12+
:local:
13+
14+
Prerequisites
15+
-------------
16+
17+
Before starting this tutorial, it is recommended to have completed the following tutorials on launching nodes:
18+
19+
* :doc:`Launching Multiple Nodes <../../Beginner-CLI-Tools/Launching-Multiple-Nodes/Launching-Multiple-Nodes>`
20+
* :doc:`Creating Launch files <../../Intermediate/Launch/Creating-Launch-Files>`
21+
22+
Background
23+
----------
24+
25+
Where unit tests focus on validating a very specific piece of functionality, integration tests focus on validating the interaction between pieces of code.
26+
In ROS 2 this is often accomplished by launching a system of one or several nodes, for example the `Gazebo simulator <https://gazebosim.org/home>`__ and the `Nav2 navigation <https://github.com/ros-planning/navigation2.git>`__ stack.
27+
As a result, these tests are more complex both to set up and to run.
28+
29+
A key aspect of ROS 2 integration testing is that nodes that are part of different tests shouldn't communicate with each other, even when run in parallel.
30+
This will be achieved here using a specific test runner that picks unique :doc:`ROS domain IDs <../../../Concepts/Intermediate/About-Domain-ID>`.
31+
In addition, integration tests have to fit in the overall testing workflow.
32+
A standardized approach is to ensure each test outputs an XUnit file, which are easily parsed using common test tooling.
33+
34+
Overview
35+
--------
36+
37+
The main tool in use here is the `launch_testing <https://docs.ros.org/en/{DISTRO}/p/launch_testing/index.html>`_ package
38+
(`launch_testing repository <https://github.com/ros2/launch/tree/{REPOS_FILE_BRANCH}/launch_testing>`_).
39+
This ROS-agnostic functionality can extend a Python launch file with both active tests (that run while the nodes are also running) and post-shutdown tests (which run once after all nodes have exited).
40+
``launch_testing`` relies on the Python standard module `unittest <https://docs.python.org/3/library/unittest.html>`_ for the actual testing.
41+
To get our integration tests run as part of ``colcon test``, we register the launch file in the ``CMakeLists.txt``.
42+
43+
Steps
44+
-----
45+
46+
1 Describe the test in the test launch file
47+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48+
49+
Both the nodes under test and the tests themselves are launched using a Python launch file, which resembles a ROS 2 Python launch file.
50+
It is customary to make the integration test launch file names follow the pattern ``test/test_*.py``.
51+
52+
There are two common types of tests in integration testing: active tests, which run while the nodes under test are running, and post-shutdown tests, which are run after exiting the nodes.
53+
We will cover both in this tutorial.
54+
55+
1.1 Imports
56+
~~~~~~~~~~~
57+
58+
We first start by importing the Python modules we will be using.
59+
Only two modules are specific to testing: the general-purpose ``unittest``, and ``launch_testing``.
60+
61+
.. code-block:: python
62+
63+
import os
64+
import sys
65+
import time
66+
import unittest
67+
68+
import launch
69+
import launch_ros
70+
import launch_testing.actions
71+
import rclpy
72+
from turtlesim.msg import Pose
73+
74+
1.2 Generate the test description
75+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
76+
77+
The function ``generate_test_description`` describes what to launch, similar to ``generate_launch_description`` in a ROS 2 Python launch file.
78+
In the example below, we launch the turtlesim node and half a second later our tests.
79+
80+
In more complex integration test setups, you will probably want to launch a system of several nodes, together with additional nodes that perform mocking or must otherwise interact with the nodes under test.
81+
82+
.. code-block:: python
83+
84+
def generate_test_description():
85+
return (
86+
launch.LaunchDescription(
87+
[
88+
# Nodes under test
89+
launch_ros.actions.Node(
90+
package='turtlesim',
91+
namespace='',
92+
executable='turtlesim_node',
93+
name='turtle1',
94+
),
95+
# Launch tests 0.5 s later
96+
launch.actions.TimerAction(
97+
period=0.5, actions=[launch_testing.actions.ReadyToTest()]),
98+
]
99+
), {},
100+
)
101+
102+
1.3 Active tests
103+
~~~~~~~~~~~~~~~~
104+
105+
The active tests interact with the running nodes.
106+
In this tutorial, we will check whether the turtlesim node publishes pose messages (by listening to the node's 'turtle1/pose' topic) and whether it logs that it spawned the turtle (by listening to stderr).
107+
108+
The active tests are defined as methods of a class inheriting from `unittest.TestCase <https://docs.python.org/3/library/unittest.html#unittest.TestCase>`_.
109+
The child class, here ``TestTurtleSim``, contains the following methods:
110+
111+
- ``test_*``: the test methods, each performing some ROS communication with the nodes under test and/or listening to the process output (passed in through ``proc_output``).
112+
They are executed sequentially.
113+
- ``setUp``, ``tearDown``: respectively run before (to prepare the test fixture) and after executing each test method.
114+
By creating the node in the ``setUp`` method, we use a different node instance for each test to reduce the risk of tests communicating with each other.
115+
- ``setUpClass``, ``tearDownClass``: these class methods respectively run once before and after executing all the test methods.
116+
117+
It's highly recommended to go through `launch_testing's detailed documentation on this topic <https://docs.ros.org/en/{DISTRO}/p/launch_testing/index.html>`_.
118+
119+
.. code-block:: python
120+
121+
# Active tests
122+
class TestTurtleSim(unittest.TestCase):
123+
@classmethod
124+
def setUpClass(cls):
125+
rclpy.init()
126+
127+
@classmethod
128+
def tearDownClass(cls):
129+
rclpy.shutdown()
130+
131+
def setUp(self):
132+
self.node = rclpy.create_node('test_turtlesim')
133+
134+
def tearDown(self):
135+
self.node.destroy_node()
136+
137+
def test_publishes_pose(self, proc_output):
138+
"""Check whether pose messages published"""
139+
msgs_rx = []
140+
sub = self.node.create_subscription(
141+
Pose, 'turtle1/pose',
142+
lambda msg: msgs_rx.append(msg), 100)
143+
try:
144+
# Listen to the pose topic for 10 s
145+
end_time = time.time() + 10
146+
while time.time() < end_time:
147+
# spin to get subscriber callback executed
148+
rclpy.spin_once(self.node, timeout_sec=1)
149+
# There should have been 100 messages received
150+
assert len(msgs_rx) > 100
151+
finally:
152+
self.node.destroy_subscription(sub)
153+
154+
def test_logs_spawning(self, proc_output):
155+
"""Check whether logging properly"""
156+
proc_output.assertWaitFor(
157+
'Spawning turtle [turtle1] at x=',
158+
timeout=5, stream='stderr')
159+
160+
Note that the way we listen to the 'turtle1/pose' topic in ``test_publishes_pose`` differs from :doc:`the usual approach <../../Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber>`.
161+
Instead of calling the blocking ``rclpy.spin``, we trigger the ``spin_once`` method - which executes the first available callback (our subscriber callback if a message arrived within 1 second) - until we have gathered all messages published over the last 10 seconds.
162+
The package `launch_testing_ros <https://docs.ros.org/en/{DISTRO}/p/launch_testing_ros/index.html>`_ provides some convenience functions to achieve similar behavior,
163+
such as `WaitForTopics <https://docs.ros.org/en/{DISTRO}/p/launch_testing_ros/launch_testing_ros.wait_for_topics.html>`_.
164+
165+
If you want to go further, you can implement a third test that publishes a twist message, asking the turtle to move, and subsequently checks that it moved by asserting that the pose message changed.
166+
This effectively automates part of the `Turtlesim introduction tutorial <../../Beginner-CLI-Tools/Introducing-Turtlesim/Introducing-Turtlesim>`.
167+
168+
1.4 Post-shutdown tests
169+
~~~~~~~~~~~~~~~~~~~~~~~
170+
171+
The classes marked with the ``launch_testing.post_shutdown_test`` decorator are run after letting the nodes under test exit.
172+
A typical test here is whether the nodes exited cleanly, for which ``launch_testing`` provides the method
173+
`asserts.assertExitCodes <https://docs.ros.org/en/{DISTRO}/p/launch_testing/launch_testing.asserts.html#launch_testing.asserts.assertExitCodes>`_.
174+
175+
.. code-block:: python
176+
177+
# Post-shutdown tests
178+
@launch_testing.post_shutdown_test()
179+
class TestTurtleSimShutdown(unittest.TestCase):
180+
def test_exit_codes(self, proc_info):
181+
"""Check if the processes exited normally."""
182+
launch_testing.asserts.assertExitCodes(proc_info)
183+
184+
2 Register the test in the CMakeLists.txt
185+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
186+
187+
Registering the test in the ``CMakeLists.txt`` fulfills two functions:
188+
189+
- it integrates it in the ``CTest`` framework ROS 2 CMake-based packages rely on
190+
(and hence it will be called when running ``colcon test``).
191+
- it allows to specify *how* the test is to be run -
192+
in this case, with a unique domain id to ensure test isolation.
193+
194+
This latter aspect is realized using the special test runner `run_test_isolated.py <https://github.com/ros2/ament_cmake_ros/blob/{REPOS_FILE_BRANCH}/ament_cmake_ros/cmake/run_test_isolated.py>`_.
195+
To ease adding several integration tests, we define the CMake function ``add_ros_isolated_launch_test`` such that each additional test requires only a single line.
196+
197+
.. code-block:: cmake
198+
199+
cmake_minimum_required(VERSION 3.8)
200+
project(app)
201+
202+
########
203+
# test #
204+
########
205+
206+
if(BUILD_TESTING)
207+
# Integration tests
208+
find_package(ament_cmake_ros REQUIRED)
209+
find_package(launch_testing_ament_cmake REQUIRED)
210+
function(add_ros_isolated_launch_test path)
211+
set(RUNNER "${ament_cmake_ros_DIR}/run_test_isolated.py")
212+
add_launch_test("${path}" RUNNER "${RUNNER}" ${ARGN})
213+
endfunction()
214+
add_ros_isolated_launch_test(test/test_integration.py)
215+
endif()
216+
217+
3 Dependencies and package organization
218+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
219+
220+
Finally, add the following dependencies to your ``package.xml``:
221+
222+
.. code-block:: XML
223+
224+
<test_depend>ament_cmake_ros</test_depend>
225+
<test_depend>launch</test_depend>
226+
<test_depend>launch_ros</test_depend>
227+
<test_depend>launch_testing</test_depend>
228+
<test_depend>launch_testing_ament_cmake</test_depend>
229+
<test_depend>rclpy</test_depend>
230+
<test_depend>turtlesim</test_depend>
231+
232+
After following the above steps, your package (here named 'app') ought to look as follows:
233+
234+
.. code-block::
235+
236+
app/
237+
CMakeLists.txt
238+
package.xml
239+
tests/
240+
test_integration.py
241+
242+
Integration tests can be part of any ROS package.
243+
One can dedicate one or more packages to just integration testing, or alternatively add them to the package of which they test the functionality.
244+
In this tutorial, we go with the first option as we will test the existing turtlesim node.
245+
246+
4 Running tests and report generation
247+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
248+
249+
For running the integration test and examining the results, see the tutorial :doc:`Running Tests in ROS 2 from the Command Line<../../Intermediate/Testing/CLI>`.
250+
251+
Summary
252+
-------
253+
254+
In this tutorial, we explored the process of creating and running integration tests on the ROS 2 turtlesim node.
255+
We discussed the integration test launch file and covered writing active tests and post-shutdown tests.
256+
To recap, the four key elements of the integration test launch file are:
257+
258+
* The function ``generate_test_description``: This launches our nodes under tests as well as our tests.
259+
* ``launch_testing.actions.ReadyToTest()``: This alerts the test framework that the tests should be run, and ensures that the active tests and the nodes are run together.
260+
* An undecorated class inheriting from ``unittest.TestCase``: This houses the active tests, including set up and teardown, and gives access to ROS logging through ``proc_output``.
261+
* A second class inheriting from ``unittest.TestCase`` decorated with ``@launch_testing.post_shutdown_test()``: These are tests that run after all nodes have shutdown; it is common to assert that the nodes exited cleanly.
262+
263+
The launch test is subsequently registered in the ``CMakeLists.txt`` using the custom cmake macro ``add_ros_isolated_launch_test`` which ensures that each launch test runs with a unique ``ROS_DOMAIN_ID``,
264+
avoiding undesired cross communication.
265+
266+
Related content
267+
---------------
268+
269+
* :doc:`Why automatic tests? <../../Intermediate/Testing/Testing-Main>`
270+
* :doc:`C++ unit testing with GTest <../../Intermediate/Testing/Cpp>`
271+
and :doc:`Python unit testing with Pytest <../../Intermediate/Testing/Python>`
272+
* `launch_pytest documentation <https://docs.ros.org/en/{DISTRO}/p/launch_pytest/index.html>`_,
273+
an alternative launch integration testing package to ``launch_testing``

source/Tutorials/Intermediate/Testing/Testing-Main.rst

+1
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ Available Tutorials:
4141
CLI
4242
Cpp
4343
Python
44+
Integration
4445
BuildFarmTesting

0 commit comments

Comments
 (0)