Skip to content

Commit 0da3412

Browse files
Apply feedback from Javi, Mich and Gera (#31)
* Apply feedback from Javi and Mich * refactor TDD * clarify and add exemple EXPECT vs ASSERT * Address comments and try to solve the specific-ci problem * document pre-commit in modules 1 and 6 * Minor modifications in guide * Rename Module 1 * Minor change --------- Co-authored-by: JesusSilvaUtrera <jesus.silva@ekumenlabs.com> Co-authored-by: Xavier Ruiz <xavier.ruiz@ekumenlabs.com>
1 parent 6bfbf53 commit 0da3412

File tree

10 files changed

+155
-50
lines changed

10 files changed

+155
-50
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@ By the end of the workshop, participants will be able to:
5151

5252
## 📋 Workshop structure
5353

54-
This workshop is organized into six modules that progressively develop the participant’s understanding of **testing in ROS 2**, from code quality fundamentals to complete Continuous Integration pipelines.
54+
This workshop is organized into six modules that progressively develop the participant’s understanding of **testing in ROS 2**, from static analysis fundamentals to complete Continuous Integration pipelines.
5555

5656
Each module combines conceptual material with practical exercises that apply the ideas directly to real ROS 2 code. All exercises are designed to be executed in a consistent environment using the provided Docker setup.
5757

5858
> [!IMPORTANT]
5959
> Before starting, build the Docker environment provided for this workshop. It includes all dependencies and tools required for the exercises. Follow the detailed instructions in the [Docker README](./docker/README.md).
6060
61-
1. **[Module 1 – Linters](modules/module_1/README.md)**
61+
1. **[Module 1 – Static Analysis Tools](modules/module_1/README.md)**
6262

63-
Understand how automated linters and static analysis tools enforce consistency, readability, and safety across ROS 2 codebases.
63+
Understand how automated formatters, linters and static analyzers enforce consistency, readability, and safety across ROS 2 codebases.
6464

6565
2. **[Module 2 – Unit Testing](modules/module_2/README.md)**
6666

docker/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ ENV DEBIAN_FRONTEND=noninteractive
66
ENV ROS_DISTRO=jazzy
77

88
# Copy requirement files and install dependencies (ignore comments and empty lines)
9-
COPY docker/requirements.txt .
9+
COPY docker/apt_packages.txt .
1010
RUN apt-get update && \
11-
apt-get install --no-install-recommends -y $(grep -vE '^\s*#' requirements.txt | grep -vE '^\s*$') && \
11+
apt-get install --no-install-recommends -y $(grep -vE '^\s*#' apt_packages.txt | grep -vE '^\s*$') && \
1212
rm -rf /var/lib/apt/lists/*
13-
RUN rm requirements.txt
13+
RUN rm apt_packages.txt
1414

1515
# Some dependencies need to be installed with pip instead of apt
1616
RUN apt-get update && apt-get install -y --no-install-recommends python3-pip && \

modules/module_1/README.md

Lines changed: 82 additions & 24 deletions
Large diffs are not rendered by default.

modules/module_2/README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,19 @@ A few core concepts are especially useful:
7979
- `EXPECT_*` records a failure but allows the test to continue.
8080
- `ASSERT_*` aborts the test immediately on failure.
8181
82-
Use `EXPECT_*` for checks that can accumulate, and `ASSERT_*` when later steps would be meaningless if the check fails.
82+
Consider testing that a function returns a `std::vector<int>` with the right length and expected contents:
83+
84+
```cpp
85+
std::vector<int> vec = get_vector();
86+
// If the length is wrong, further checks (indexing) would be invalid -> abort test
87+
ASSERT_EQ(3u, vec.size()); // stop the test immediately if size != 3
88+
// Now it is safe to check contents; these can be EXPECT so we see all mismatches at once
89+
EXPECT_EQ(10, vec[0]);
90+
EXPECT_EQ(20, vec[1]);
91+
EXPECT_EQ(30, vec[2]);
92+
```
93+
94+
Use `ASSERT_*` for preconditions that must hold for remaining assertions to make sense (avoid crashes and meaningless failures). Use `EXPECT_*` for value checks where continuing to run the test to collect multiple failures is useful
8395
8496
- **Fixtures**: allows code to be reused across multiple tests. Define a test class deriving from `::testing::Test` and use `TEST_F` instead of `TEST`.
8597
- **Parameterized tests**: The same test logic can be executed against multiple input values with `TEST_P`. This reduces duplication and is especially helpful when validating algorithms across many corner cases
@@ -142,17 +154,17 @@ ROS 2 wraps GoogleTest/GoogleMock with lightweight CMake helpers so tests build
142154

143155
## How to Write Tests
144156

145-
Writing good unit tests is as much about structure as it is about logic. Two key concepts guide this process: **Test-Driven Development (TDD)** and the **Arrange-Act-Assert (AAA)** pattern.
146-
147-
**Test-Driven Development (TDD)** is an iterative approach where tests are written before the actual code. Each cycle begins by defining a small, failing test that expresses a desired behavior. The minimal code needed to make the test pass is then implemented, followed by a short refactoring step to clean up or generalize the design. This rhythm of red → green → refactor encourages clear requirements, modular code, and continuous verification.
148-
149-
The **AAA** pattern provides a simple mental model for structuring each test.
157+
A good unit test is clear, concise, and focused. The best way to achieve this is by following the Arrange-Act-Assert (AAA) pattern, which provides a simple mental model for structuring each test:
150158

151159
- **Arrange**: prepare the environment, inputs, and objects needed for the test.
152160
- **Act**: execute the function or behavior being tested.
153161
- **Assert**: verify that the observed result matches the expected outcome.
154162

155-
Following this structure makes tests easy to read, maintain, and reason about. Each test should describe one behavior clearly, without hidden dependencies or side effects.
163+
Following this pattern leads to tests that are consistent, self-explanatory, and easy to debug when they fail.
164+
165+
Beyond how tests are written, it’s also important to consider when they are written. This leads to a popular development workflow known as **Test-Driven Development (TDD)**. TDD follows an iterative approach where tests are written before the actual code. Each cycle begins by defining a small, failing test that expresses a desired behavior. The minimal code needed to make the test pass is then implemented, followed by a short refactoring step to clean up or generalize the design. This rhythm of red → green → refactor encourages clear requirements, modular code, and continuous verification.
166+
167+
While TDD helps drive better design decisions and encourages modular, testable architectures, the same testing principles can be applied in traditional “test-after” workflows. The key takeaway is that **testability should guide design**, regardless of whether tests come before or after the code.
156168

157169
## Exercises
158170

@@ -187,4 +199,4 @@ The task is complete when tests are run and the output shows **0 errors** and **
187199
- [Google Test Repo](https://github.com/google/googletestl)
188200
- [Google Test Macros](https://google.github.io/googletest/reference/testing.html)
189201
- [Google Test Assertions](https://google.github.io/googletest/reference/assertions.html)
190-
- [Google Mock Basics](https://google.github.io/googletest/gmock_for_dummies.html)
202+
- [Google Mock Basics](https://google.github.io/googletest/gmock_for_dummies.html)

modules/module_4/README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ In this module, the focus is to explore **integration testing in ROS 2** to veri
77
- [Motivation](#motivation)
88
- [The launch\_testing Framework](#the-launch_testing-framework)
99
- [Registering the tests](#registering-the-tests)
10+
- [Avoiding flaky tests](#avoiding-flaky-tests)
1011
- [Alternative: launch\_pytest](#alternative-launch_pytest)
1112
- [Exercises](#exercises)
1213
- [Exercise 1](#exercise-1)
@@ -101,6 +102,28 @@ And in the `package.xml`:
101102
<test_depend>launch_testing_ament_cmake</test_depend>
102103
```
103104

105+
### Avoiding flaky tests
106+
107+
The most common mistake in integration testing is writing a **flaky test**. A flaky test is one that passes sometimes and fails other times, even when no code has changed. This is almost always caused by a race condition.
108+
109+
Flaky (Bad) Test Logic:
110+
111+
1. Launch nodes.
112+
2. Immediately publish a message (for example, on `/scan`).
113+
3. Check for an expected result (for example, a log message).
114+
115+
**Why it fails**: The nodes in `generate_test_description` are launched, but they are not guaranteed to be ready or subscribed to their topics by the time the test case runs. The `ReadyToTest()` action only means the launch process is complete. If the test publishes its message before the node is subscribed, the message is dropped, and the test fails.
116+
117+
Reliable (Good) Test Logic:
118+
119+
1. Launch nodes.
120+
2. In the test case, create the publisher.
121+
3. Wait for the system to be ready. A simple, robust way is to wait until the publisher sees that a subscriber is connected.
122+
4. Once the subscription has been confirmed, then publish the message.
123+
5. Check for the expected result.
124+
125+
This event-driven approach is deterministic and eliminates the race condition.
126+
104127
### Alternative: launch_pytest
105128

106129
While using `launch_testing` with `unittest` is the classic approach used, support for more modern approaches like using `pytest` is also available. `Pytest` is a powerful and modern third party framework (`unittest` is part of the Python standard library) that has become the most used option for Python testing in the community. It is also gaining popularity within the ROS ecosystem.
@@ -130,7 +153,7 @@ Now, run the tests. This will fail because the test script is incomplete:
130153
colcon test --packages-up-to module_4 --event-handlers console_direct+
131154
```
132155

133-
The incomplete test script already handles launching the nodes. You need to fill in the logic inside the `unittest.TestCase` to verify its behavior.
156+
The incomplete test script already handles launching the nodes. Fill in the logic inside the `unittest.TestCase` to verify its behavior.
134157

135158
The additions to the Python test script must:
136159

modules/module_4/test/test_detection_launch.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44

55
from launch import LaunchDescription
6-
from launch_ros.actions import Node, ComposableNodeContainer
6+
from launch_ros.actions import ComposableNodeContainer
77
from launch_ros.descriptions import ComposableNode
88
import launch_testing
99
import launch_testing.actions
@@ -109,12 +109,23 @@ def test_obstacle_triggers_red_light(self, proc_output):
109109
#
110110
# 1. Create a publisher to the /scan topic.
111111
#
112-
# 2. Create and publish a LaserScan message that will trigger the detector.
113-
# Use the helper function.
112+
# 2. Wait for the subscriber to be ready (THIS IS THE CRITICAL PART!)
113+
# - Create a loop that checks `self.scan_publisher.get_subscription_count()`
114+
# - Use a timeout (for example 10 seconds) to prevent an infinite loop.
115+
# - Inside the loop, spin the node (`rclpy.spin_once`)
116+
# - After the loop, use `self.assertGreater` to fail the test if
117+
# no subscriber appeared.
114118
#
115-
# 3. Use 'proc_output.assertWaitFor' to check for the "RED LIGHT" message.
119+
# 3. Create and publish a LaserScan message that will trigger the detector.
120+
# Use the helper function above.
116121
#
117-
assert False # Replace this 'pass' statement with your test logic
122+
# 4. Use 'proc_output.assertWaitFor' to check for the "RED LIGHT" message.
123+
# - Give it a timeout (for example, 5 seconds).
124+
#
125+
# 5. (Optional but good practice) Add a try/except block around
126+
# `assertWaitFor` to provide a clearer failure message if it times out.
127+
#
128+
assert False # Replace this 'assert' with the necessary code for the test
118129
# ====================== END EDIT ==================================
119130

120131

modules/module_5/README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,9 @@ Its purpose is to validate the robot's ability to meet a high-level requirement,
3333

3434
The importance of E2E testing in robotics includes:
3535

36-
<!-- TODO: Review this, and see if it matches more with field debugging -->
37-
3836
- **Validating the "Mission"**: It's the only test level that answers the question: "Does the robot actually achieve its goal?"
3937
- **Testing Against Reality**: By using data recorded from the real world (or a high-fidelity simulator), rosbags provide a "ground truth" scenario. This makes possible to test complex, emergent behaviors and edge cases that are impossible to script in a simple integration test.
4038
- **Ultimate Regression-Proofing**: An E2E test is the ultimate safety net. If a change in any package (perception, control, navigation) breaks the robot's ability to complete its mission, a good E2E test will catch it.
41-
- **Debugging Complex Failures**: When a robot fails in the field, a rosbag of that failure is invaluable. It can be replayed in a simulator over and over until the root cause (for example, a race condition, a state machine logic error) is found.
4239

4340
## The rosbag Toolset
4441

@@ -104,7 +101,7 @@ This command acts like a "data simulator", providing a perfectly repeatable stre
104101

105102
This is the most common and intuitive form of E2E testing. It involves a human operator launching the system, providing a scenario (usually via a rosbag), and visual or log-based verification of the result.
106103

107-
This is perfect for debugging, or for a final "sanity check" before merging a major feature.
104+
This is perfect for a final "sanity check" before merging a major feature.
108105

109106
### Using Pre-recorded Rosbags
110107

@@ -115,7 +112,6 @@ A typical manual test session looks like this:
115112
3. Observe and Verify: The engineer watches the output:
116113
- In `RViz`: "Does the robot's navigation visualization show it reaching the goal?"
117114
- In the terminal: "Did the mission control node log 'MISSION_COMPLETE'?"
118-
4. Analyze: If it fails, now it's possible to debug the running nodes, knowing the input data is identical every single time.
119115

120116
This workflow is incredibly powerful but has one major drawback: it's not automated. It relies on a human to launch, observe, and judge success.
121117

modules/module_6/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ Integrating CI into the workflow is essential because it:
4848

4949
For projects hosted on GitHub, the easiest and most popular way to implement CI is with **GitHub Actions**.
5050

51+
> [!TIP]
52+
> **Use Pre-commit Hooks to Optimize Your Workflow**
53+
>
54+
> While **CI is the mandatory quality gate** for merging, it's often faster for developers to catch simple style errors **locally** before they push. Tools like **pre-commit hooks** (as discussed in Module 1) run fast checks like formatting locally, saving the developer time waiting for the CI pipeline to run just to fail on a style inconsistency. They complement the CI by ensuring your commits are clean and focused on functional changes.
55+
5156
### Introduction to GitHub Actions
5257

5358
GitHub Actions is a CI/CD platform built directly into GitHub. Automation workflows are defined in a **YAML file** in a special directory in the repository: `.github/workflows/`. GitHub automatically detects these files and runs them based on a set of custom-defined rules or triggers, such as when code is pushed, a pull request is opened, or a scheduled job is due.

tools/run_ci_build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ rosdep install --from-paths modules --ignore-src -y
2727
colcon build --packages-up-to "$@" --symlink-install --event-handlers console_direct+
2828

2929
# Test
30-
colcon test --packages-up-to "$@" --event-handlers console_direct+
30+
colcon test --packages-up-to "$@" --event-handlers console_direct+ --return-code-on-test-failure

0 commit comments

Comments
 (0)