Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ROS 2 Testing: A Practical Survival Guide

This workshop introduces the **best practices for designing, testing, and maintaining ROS 2 nodes in C++** to ensure code quality, maintainability, and confidence in deployments.
This workshop, prepared for ROSCon España 2025, introduces the **best practices for designing, testing, and maintaining ROS 2 nodes in C++** to ensure code quality, maintainability, and confidence in deployments.
Comment thread
xaru8145 marked this conversation as resolved.
Outdated

It combines theory with hands-on exercises so that participants can directly apply the concepts in their own workflow.

Expand Down Expand Up @@ -85,3 +85,10 @@ Each module combines conceptual material with practical exercises that apply the
---

For detailed explanations and references, see the individual module READMEs in the [modules](./modules) directory.

---

## ✍️ Authors

* **[Jesús Silva Utrera]** - [@JesusSilvaUtrera](https://github.com/JesusSilvaUtrera)
* **[Xavier Ruiz Vilda]** - [@xaru8145](https://github.com/xaru8145)
6 changes: 3 additions & 3 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ It's possible to get information about the available flags for both scripts usin

Once the container is started, the entrypoint is located at the root of the ROS 2 workspace (`~/ws`).

After shutdown, the user will be asked whether to save the changes made inside the container or not to the image (performing a `docker commit` internally).
After exiting the container, the user will be asked whether to save the changes back to the image (internally performing a `docker commit`).

If you need to open another terminal connected to the same container, use the helper script [join.sh](./join.sh):
To open another terminal connected to the same container, use the helper script [join.sh](./join.sh):

```bash
./docker/join.sh
```

By default, this opens an interactive **bash** session inside the running container named `ros2-testing-workshop-roscon-es-25-container`. You can specify a different container name and command, use the `-h` flag for details.
By default, the script opens an interactive **bash** session inside the running container named `ros2-testing-workshop-roscon-es-25-container`. A different container name or command can be specified; use the `-h` flag for details.
15 changes: 15 additions & 0 deletions docker/build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
#!/usr/bin/env bash

# Copyright 2025 Ekumen, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -e

# Default values for configuration
Expand Down
15 changes: 15 additions & 0 deletions docker/ros_entrypoint_dev.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
#!/usr/bin/env bash

# Copyright 2025 Ekumen, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -euo pipefail

# runtime-friendly defaults
Expand Down
15 changes: 15 additions & 0 deletions docker/run.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
#!/usr/bin/env bash

# Copyright 2025 Ekumen, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -e

# Configuration
Expand Down
14 changes: 14 additions & 0 deletions modules/module_1/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2025 Ekumen, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

cmake_minimum_required(VERSION 3.8)
project(module_1)

Expand Down
2 changes: 1 addition & 1 deletion modules/module_1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ source install/setup.bash
colcon lint --packages-select module_1
```

Some indications will appear. The task is to correct these indications, adding the missing dependencies and ficing the style inconsistencies.
Some indications will appear. The task is to correct these indications, adding the missing dependencies and fixing the style inconsistencies.

#### Definition of success

Expand Down
14 changes: 14 additions & 0 deletions modules/module_2/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2025 Ekumen, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

cmake_minimum_required(VERSION 3.8)
project(module_2)

Expand Down
75 changes: 53 additions & 22 deletions modules/module_2/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# Module 2 – Unit Testing

In this module, the focus is on **unit tests** in ROS 2, with an emphasis on designing testable code and validating core algorithms using gtest and gmock.
In this module, the focus is on **unit tests** in ROS 2, with an emphasis on designing testable code and validating core algorithms using `gtest` and `gmock`.

- [Module 2 – Unit Testing](#module-2--unit-testing)
- [Objectives](#objectives)
- [Motivation](#motivation)
- [Testable Design](#testable-design)
- [Example](#example)
- [GoogleTest](#googletest)
- [Ament Integration](#ament-integration)
- [How to Write Tests](#how-to-write-tests)
- [Exercises](#exercises)
- [What is a Laser Scan?](#what-is-a-laser-scan)
- [Exercise 1](#exercise-1)
- [Definition of success](#definition-of-success)
- [Exercise 2](#exercise-2)
- [Definition of success](#definition-of-success-1)
- [References](#references)

## Objectives
Expand Down Expand Up @@ -51,21 +53,11 @@ In ROS 2, **SRP** and **DIP** are the most relevant. Separating algorithmic logi

A recommended practice is to implement core algorithms using ROS-agnostic libraries such as `Eigen` for linear algebra or `PCL` for point cloud processing. This reduces coupling to ROS distribution APIs, ensures portability across ROS versions, and makes the algorithm easier to test and maintain over time.

### Example
> [!NOTE]
> Complete [Exercise 1](#exercise-1) before continuing with the next section.
Comment thread
xaru8145 marked this conversation as resolved.
Outdated
>
> The exercise provides a practical example of how tightly coupled code that mixes computation, state, and ROS interfaces becomes difficult to test in isolation.

How could the filtering logic be tested in the example below? The callback mixes computation, state, logging, and publishing: does this follow the Single Responsibility Principle (SRP)?

```cpp
void callback(const std_msgs::msg::Float64 & msg) {
double value = msg.data;

double filtered = 0.9 * prev_ + 0.1 * value;
prev_ = filtered;

RCLCPP_INFO(this->get_logger(), "Filtered = %f", filtered);
publisher_->publish(std_msgs::msg::Float64{filtered});
}
```

## GoogleTest

Expand Down Expand Up @@ -168,29 +160,68 @@ While TDD helps drive better design decisions and encourages modular, testable a

## Exercises

The exercises for this module focus on transforming non-testable code into testable code and validating the core logic using the GoogleTest framework.
The exercises in this module focus on identifying code that is hard to test, then completing and verifying a refactored, testable implementation with the GoogleTest framework.

### What is a Laser Scan?

A Laser Scanner (LiDAR) provides a **2D** view of the robot’s surroundings by measuring **distances** to nearby obstacles across a range of **angles**.

The data is often used for tasks such as **mapping**, **localization**, **obstacle detection**, and **collision avoidance**. In the upcoming exercises, this scan data is used to build and test a simple laser detector that identifies when obstacles appear within a predefined region around the robot.

In ROS 2, these readings are published as a [sensor_msgs/msg/LaserScan](https://docs.ros.org/en/ros2_packages/jazzy/api/sensor_msgs/msg/LaserScan.html) message, which contains:

- `ranges`: a vector of distance values, one per beam. Values like `+inf` indicate no object was detected (free space up to max range), while values near zero or `NaN` often indicate an invalid reading.
- `angle_min`, `angle_max`, and `angle_increment`: define the angular coverage and spacing between beams.
- `range_min` and `range_max`: valid measurement bounds of the sensor.

Each element in `ranges` therefore corresponds to a specific direction and distance measurement, forming the basis for algorithms that filter regions of interest or detect obstacles within a defined footprint.

### Exercise 1

The objective of this exercise is to identify and refactor code that violates the Single Responsibility Principle (SRP) and is therefore hard to test.
Begin by examining [bad_laser_detector.cpp](src/bad_laser_detector.cpp), a node that processes LiDAR scans while also handling ROS 2 communication. Take a moment to understand what the node does and how its logic is structured.

Begin by examining [bad_laser_detector.cpp](src/bad_laser_detector.cpp) node that processes LiDAR scans, combining publishers, subscribers, and algorithmic logic in a single callback. This coupling between ROS interfaces and computation makes the algorithm untestable in isolation, as every test would require launching ROS infrastructure.
As the code is reviewed, consider the following questions:

Next, review the `LaserDetector` class defined in [laser_detector.cpp](src/laser_detector.cpp), which isolates the core obstacle detection algorithm from the ROS 2 node. The task is to complete the missing parts of this class so that the algorithm becomes fully testable and all provided unit tests pass.
- Can the core detection logic be tested without running a ROS system?
- What parts of the code make testing easier or harder?
- How might you restructure it so that the functionality could be tested independently from ROS interfaces?

Do not make any changes yet, this exercise is purely about inspection and reasoning about testability.

#### Definition of success

You can clearly explain why the file is or isn’t testable and identify the main obstacles that make unit testing hard.

### Exercise 2

Review the `LaserDetector` class in [laser_detector.cpp](src/laser_detector.cpp), a refactored version of the node implemented in [bad_laser_detector.cpp](src/bad_laser_detector.cpp). This class isolates the core obstacle detection algorithm from the ROS 2 interfaces. The task is to complete the missing parts of this class so that the algorithm becomes fully testable and all provided unit tests pass.

The following components require completion:

- Constructor: Complete the missing input validation checks to make tests pass.
- `roi_filter`: Implement the logic to iterate through the input ranges, check the angle against the ROI and return a new vector with the filtered data.
- `points_inside_footprint`: Implement the logic to count how many ranges are finite and less than or equal to `footprint_radius_`.
- `detect_obstacle`: Implement the final comparison logic. A detection is true if `num_points≥min_points_`.

Inspect the tests in [test_laser_detector.cpp](test/test_laser_detector.cpp) to understand the expected behavior. Progress can be verified incrementally by rebuilding and running the tests after each step. To rebuild the package:

```bash
cd ~/ws
colcon build --packages-up-to module_2 --event-handlers console_direct+
source install/setup.bash
```

Then run the tests:

```bash
colcon test --packages-select module_2 --event-handlers console_direct+
```

> [!NOTE]
> This exercise demonstrates Test-Driven Development (TDD) in practice: using predefined unit tests to guide the implementation of a clear, modular, and testable design.

#### Definition of success

The task is complete when tests are run and the output shows **0 errors** and **0 failures** for the `TestLaserDetector` suite defined in [test_laser_detector.cpp](test/test_laser_detector.cpp).
The task is complete when **tests are run** and the output shows **0 errors** and **0 failures** for the `TestLaserDetector` suite defined in [test_laser_detector.cpp](test/test_laser_detector.cpp).

## References

Expand Down
22 changes: 16 additions & 6 deletions modules/module_2/src/laser_detector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,23 @@ std::vector<float> LaserDetector::roi_filter(const std::vector<float>& scan) {
if (scan.empty()) {
return {};
}
// Return a filtered scan that fits within the angle ROI (Region of Interest)
// provided in the constructor.

std::vector<float> scan_out;
scan_out.reserve(scan.size()); // upper bound

float current_angle = laser_options_.angle_min;
for (size_t i = 0; i < scan.size(); i++) {
// Keep only beams whose angle lies within the ROI (inclusive)
if (current_angle >= roi_min_angle_ && current_angle <= roi_max_angle_) {
scan_out.push_back(scan[i]);
}
// Update angle
current_angle += laser_options_.angle_increment;
}

/// BEGIN EDIT ------------------------------------------------------

// Return a filtered scan that fits within the angle ROI provided in
// the constructor.

/// END EDIT --------------------------------------------------------
return scan_out;
}

int LaserDetector::points_inside_footprint(const std::vector<float>& scan) {
Expand Down
14 changes: 14 additions & 0 deletions modules/module_2/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2025 Ekumen, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

find_package(ament_cmake_gtest REQUIRED)

ament_add_gtest(test_laser_detector
Expand Down
14 changes: 14 additions & 0 deletions modules/module_3/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2025 Ekumen, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

cmake_minimum_required(VERSION 3.8)
project(module_3)

Expand Down
Loading
Loading