Transformer le développement ROS 2 avec rtest
Imagine your team has just delivered a key feature in a commercial robotics system. You know the codebase is in bad shape. As a developer, you feel it every day. But leadership is focused on delivering features, not quality. So you keep moving fast, stacking up technical debt. The system grows more complex. CI pipelines start to flake. Everyone knows they’re unreliable, but no one addresses it – it’s easier to ignore than to fix. Eventually, it breaks down. Pipelines fail more often than they pass. No one can push safely. Deployment becomes a gamble. Then, it hits. A critical hotfix needed yesterday. And the whole delivery system is too fragile to respond.
Ce problème ne cessait d'apparaître dans les projets de robotique – en particulier ceux construits sur ROS 2. Ce qui aurait dû être un développement simple s'est transformé en une bataille quotidienne contre des tests instables et imprévisibles.
We had hundreds of tests, but no real confidence in them. Every code change triggered multiple CI runs, eating up hours of developer time. Frustration set in. Engineers weren’t coding – they were waiting. And worse, they were waiting on results they couldn’t trust.
It wasn’t just annoying. It was slowing everything down. As the project grew, so did the chaos. Developers spent more time debugging the tests than building the product. Releases felt like rolling the dice – a failed test might mean a real issue, or it might be nothing at all. Either way, it chipped away at trust in the test suite. Everyone got more cautious. Progress stalled.
L'un de nos clients a décidé de briser ce cycle. Fini les solutions provisoires et les correctifs ponctuels. Il avait besoin d'une véritable solution – une meilleure base pour les tests sous ROS 2.
Comprendre le défi des tests ROS 2
To fix the problem, we first needed to understand it. The root cause wasn’t our test code. It was the nature of ROS 2 itself. Built for distributed, modular robotics systems, ROS 2 is incredibly powerful, but its architecture makes unit- testing complex by design.
At the core is an asynchronous communication model. Messages pass between nodes through multiple layers, all the way down to middleware implementations like DDS. This flexibility is great for real-world applications, but it introduces unpredictability during test execution. Message timing, race conditions, and system state can all vary from one test run to the next.
En bref : ROS 2 n'a pas été conçu pour les tests unitaires isolés dès le départ. C'est ce que nous avons entrepris de changer.

L'architecture de ROS 2, bien que puissante pour la robotique distribuée, introduit une tempête parfaite de variables qui rendent difficile l'obtention de résultats de tests cohérents.
Timing is one of the biggest culprits. System load and CPU scheduling can vary between test runs, leading to different outcomes even with the same code. Hardware differences between local environments and CI pipelines can change how processes behave. Add to that the nondeterministic nature of middleware communications and multithreaded operations, and you get race conditions and timing-dependent failures that appear randomly and disappear just as quickly.
La découverte de services ajoute une couche de complexité supplémentaire.
ROS 2 nodes rely on DDS to find each other through timing-sensitive handshakes, QoS negotiations, and broadcast messages. In test environments, this process can behave differently depending on system load, network conditions, or even the sequence in which nodes launch. A test might pass when the system is idle, but fail under stress and not because of a bug, but because node discovery took slightly too long.
Il convient d'ajouter que des problèmes tels que la découverte de services et les conditions de concurrence peuvent probablement être résolus – d'une manière ou d'une autre. Mais les résoudre implique généralement un effort supplémentaire, du code répétitif et aucune garantie que la solution soit extensible à tous les scénarios.
Certes, Fast DDS propose une découverte statique pour contourner certains de ces problèmes. Mais voulons-nous vraiment nous plonger dans ce niveau de réglage du système simplement pour exécuter des tests unitaires ?
Nous pourrions passer à Zenoh, ou même développer notre propre middleware de test. Mais le fait que nous envisagions ce type de solutions de contournement ne fait que renforcer le problème de fond : nous n'avons pas besoin de plus de bricolage. Nous avons besoin d'une solution systématique.
Message delivery timing is also unpredictable. A publisher might send a message, but there’s no guarantee a subscriber will receive it within a fixed window. Traditional workarounds like inserting arbitrary delays or polling are fragile and tend to increase test complexity and runtime without improving reliability.
Le résultat ? Un processus de test auquel on ne peut pas faire confiance.
Un code qui fonctionne parfaitement en développement peut échouer en CI non pas parce qu'il est défectueux, mais parce que l'infrastructure ne fournit pas le déterminisme nécessaire pour le valider.
Obtenez votre guide gratuit pour migrer de ROS1 à ROS2
TéléchargerUne nouvelle approche du test ROS 2 : présentation de rtest
Après avoir été confrontés aux mêmes problèmes de test sur plusieurs projets de robotique, nous savions que la communauté n'avait pas seulement besoin de meilleures solutions de contournement. Elle avait besoin d'une meilleure base. Nous en avons donc construit une.
Rtest est un framework spécialement conçu qui apporte prévisibilité, efficacité et fiabilité aux tests unitaires ROS 2, sans nécessiter de modifications de votre code de production.
Interception ciblée au niveau de rclcpp
The key idea behind rtest is knowing exactly where to step in during ROS 2 communication. Instead of trying to control the entire middleware system, rtest smartly catches communication at the rclcpp level. That’s the C++ library that sits between your code and the lower-level ROS parts. The diagram in our documentation illustrates the typical flow of ROS 2 communication, from your nodes down through rclcpp, rcl, rmw and finally to the communication layer implementation like DDS or Zenoh.
Simuler la couche rclcpp sans modifier votre code
The beauty of rtest’s approach is that it creates mock implementations at the rclcpp layer, effectively isolating your tests from all the unpredictable elements below. Your node code remains completely unchanged, it still creates publishers, subscribers, timers and services exactly as it would in production. But instead of these components relying on the full ROS 2 stack with all its timing complexities, they connect to rtest’s controlled mock implementations. By intercepting at the rclcpp level, we maintain full API compatibility while gaining complete control over the communication flow and time delivery.
Rtest se présente comme une bibliothèque complète qui inclut un framework entier pour simuler tous les composants importants de ROS 2 :
- Abonnés,
- Éditeurs,
- Services,
- Minuteries,
- Clock.
Tout ce dont vous avez besoin est regroupé, vous n'avez donc pas à vous soucier des détails de mise en œuvre interne.

Substitution des API ROS 2
Lorsque vous incluez rtest dans votre build de test, il utilise la substitution au moment de la compilation pour rediriger les fonctions de création de composants de rclcpp vers rtest.
Your code continues to call the same create_publisher(), create_subscription() and create_service() methods, but they now return mock enabled objects instead of standard ROS 2 components. Your production code remains completely unchanged, it still calls the same APIs and follows the same patterns.
Tests isolés avec un contrôle total
Cependant, au lieu que ces composants soient acheminés via les couches middleware imprévisibles, ils se connectent directement aux implémentations simulées contrôlées de rtest.
Your tests execute in a single-threaded environment, which means you can trace through complex state changes without worrying about race conditions or timing dependent behaviours. You can examine your node’s internal state at any point and verify expected outcomes with standard debugging tools. This deterministic execution model eliminates an entire class of debugging challenges that plague traditional ROS 2 testing.
We are currently focusing our efforts on supporting the latest ROS 2 distributions. Full compatibility with ROS 2 Jazzy is already available, and we are actively working on extending support to ROS 2 Kilted Kaiju (by the time you’re reading this, it may already be ready). Older distributions will be added incrementally to ensure consistent API behaviour across versions, even as the underlying rclcpp layers evolve. Our goal is to make test upgrades as seamless as possible, allowing developers to move to newer ROS 2 versions without spending hours adapting their test code.
Intégration de CMake sans aucune friction
Rtest s'intègre de manière transparente au système de build ROS 2 grâce à des utilitaires CMake qui créent automatiquement des versions de test de vos bibliothèques existantes. L'élément clé réside dans l'intégration avec votre système de build au niveau du CMakeLists, dans la section BUILD_TESTING.
The test_tools_add_doubles function generates mock-enabled versions of your components without duplicating source code or requiring separate build configurations. The function analyses your existing target’s source files and creates a parallel test target that links against rtest’s mock implementations. Your original production target remains completely unchanged, ensuring that there’s no risk of test code accidentally affecting production builds. The build system integration also handles dependency management automatically. When you specify AMENT_DEPENDENCIES for your test doubles, the system ensures that all necessary ROS 2 packages are properly linked with their mock enabled versions.
This eliminates the tedious manual work of managing test specific dependencies and reduces the chance of configuration errors that could lead to subtle test failures. That means you can retrofit existing projects with comprehensive testing capabilities without restructuring your build configuration. Your production builds remain unchanged, while test builds automatically include the necessary mock implementations.
Écrire des tests avec rtest
Une fois cette configuration réalisée en suivant nos exemples, vous pouvez écrire vos propres tests à l'aide du framework. Voici un cas simple issu de nos exemples qui montre à quel point il est facile de tester un publisher ROS 2 pour votre nœud :
TEST_F(PubSubTest, PublishIfSubscriuptionCountNonZeroTest)
{
/// Create and find a publisher instance
auto node = std::make_shared<test_composition::Publisher>(opts);
auto publisher = rtest::findPublisher<std_msgs::msg::String>(node, "/test_topic");
/// Set subscription count to 1 to activate the successful publish logic
publisher->setSubscriptionCount(1UL);
/// Prepare the expected message that should be published
auto expectedMsg = std_msgs::msg::String{};
expectedMsg.set__data("if_subscribers_listening");
/// Set up expectation that the Node will publish a message when the subscription count is 1
EXPECT_CALL(*publisher, publish(expectedMsg)).Times(1);
/// Trigger the publishing logic in the production node
node->publishIfSubscribersListening();
}
This test shows the power of rtest in just a few lines. You create your node normally, then use rtest::findPublisher to interact with the publisher in tests. You can control things like subscription count, set up expectations for what messages should be published, and then test your node’s behavior directly.
Exécution des tests rapide, prévisible et reproductible
Les tests ROS 2 traditionnels doivent tenir compte des délais de transmission des messages, des temps de découverte des services et d'autres surcharges du middleware.
Rtest tests execute orders of magnitude faster because they bypass all network communication and run in a controlled environment. No waiting for actual ROS communication, no timing issues, no randomness, just clean predictable testing that tells you exactly whether your code works as expected. Beyond speed, the reliability improvement fundamentally changes how teams approach testing. With traditional ROS 2 tests, developers often run tests multiple times to ensure they pass consistently, a practice that wastes time and erodes confidence.
Avec rtest, un test qui réussit une fois réussira à chaque fois dans toutes les conditions, permettant aux développeurs de faire entièrement confiance à leurs résultats de test.
Utilise la syntaxe familière de Google Test
We support Google Test (gtest), which means the testing syntax is well known to what most C++ programmers are already used to – you don’t need to learn a completely new testing framework or convince your team to adopt unfamiliar tools. If you already know how to write EXPECT_CALL, ASSERT_TRUE, or TEST_F, you already know how to use rtest. This familiar approach removes one of the biggest barriers to adoption that many testing frameworks face.
Suivi automatique des composants via un registre statique
L'une des fonctionnalités les plus puissantes de rtest est son suivi automatique des composants grâce au Static Registry Pattern.
When your nodes create ROS components during initialisation, rtest automatically registers them in a centralised registry indexed by node name and topic or service name. This happens transparently without any configuration or setup code on your part. The registry implementation uses weak pointers to avoid creating circular dependencies or memory leaks. When components are destroyed, they’re automatically removed from the registry without requiring explicit cleanup code. This design ensures that the registry remains lightweight and doesn’t interfere with normal object lifecycle management.
During test execution, you can retrieve any component your node created using simple finder functions like findPublisher, findSubscription, or findService. The registry maintains weak references to avoid memory leaks and automatically handles component lifecycle management. The registry also provides introspection capabilities, allowing you to verify that your node created the expected components. You can check that a publisher was created for a specific topic, confirm service endpoints exist, or validate that timers were properly initialised.
The registry also supports more advanced scenarios, such as finding all components of a specific type created by a node, or iterating through all topics a node publishes to. This capability is particularly useful for integration tests that need to verify the complete interface of a complex node.
Exemple plus avancé : tests de services
Les services ROS 2 ajoutent une couche supplémentaire de complexité aux tests en raison de leur nature requête-réponse et de leur potentiel de défaillances réseau.
Rtest provides sophisticated mocking capabilities for both service providers and clients, allowing you to test various scenarios including service unavailability, timeout conditions, and different response patterns. For service clients, you can mock the underlying client behaviour to simulate network conditions, service failures or specific response patterns. For service providers you can directly inject requests and verify that your service logic produces the expected responses.
This level of control makes it possible to thoroughly test error handling, retry logic and edge cases that are difficult to reproduce in real network environments. If achieving high code coverage is a priority in your project, this approach is often the only reliable way to force certain testing scenarios that occur very rarely in typical runtime conditions.
Voici un exemple de test d'un client de service avec rtest :
TEST_F(ServiceClientTest, WhenServiceCallSucceeds_ThenSetStateSucceeds)
{
/// Create node
auto node = std::make_shared<test_composition::ServiceClient>(opts);
/// Retrieve the client created by the Node
auto client = rtest::findServiceClient<std_srvs::srv::SetBool>(node, "/test_service");
/// Check that the Node actually created the Client
ASSERT_TRUE(client);
/// Create successful response
auto response = std::make_shared<std_srvs::srv::SetBool::Response>();
response->success = true;
response->message = "State updated successfully";
/// Set up expectations
EXPECT_CALL(*client, service_is_ready()).WillOnce(::testing::Return(true));
/// Mock sending a service request asynchronously
EXPECT_CALL(*client, async_send_request(::testing::_))
.WillOnce([response](std::shared_ptr<std_srvs::srv::SetBool::Request>) {
std::promise<std::shared_ptr<std_srvs::srv::SetBool::Response>> promise;
promise.set_value(response);
We will do it in the future, right now we have a good starting point, let's leave this code for article purpose // Return the simulated future response
return rclcpp::ClientTypes<std_srvs::srv::SetBool>::FutureResponseAndId(
promise.get_future(),
1UL
);
});
/// Call the method that triggers the service request and validate the behavior
EXPECT_TRUE(node->setState(true));
EXPECT_TRUE(node->getLastCallSuccess());
EXPECT_EQ(node->getLastResponseMessage(), "State updated successfully");
}
Cet exemple montre comment rtest permet un contrôle précis des interactions de service, vous permettant de tester à la fois les scénarios de succès et d'échec avec une prévisibilité totale.
Avantages de rtest dans votre projet
Lorsque vous intégrez rtest dans votre projet, plusieurs transformations se produisent immédiatement.
Les tests deviennent véritablement déterministes : un test qui réussit une fois réussira à chaque fois, et lorsqu'il échoue, il échouera systématiquement, garantissant qu'aucun bug ne passe inaperçu. Cette fiabilité crée une base de confiance qui permet un développement plus rapide et plus serein.
Découvrez nos services de développement et de conseil en robotique
En savoir plusDevelopers no longer need to mentally account for test infrastructure unreliability when diagnosing failures. When a test fails it’s because of the code being tested, not because of timing issues or environmental factors. This clarity dramatically improves debugging efficiency and developer satisfaction.
The combination of faster test execution and reliable results enables more frequent testing, leading to earlier detection of issues and higher overall code quality. The framework also makes it practical to achieve high code coverage targets as edge cases and error conditions become easily testable.
Communauté et avenir
Rtest a été développé dans le cadre d'une collaboration entre Spyrosoft et Beam.
Nous tenons à remercier tout particulièrement Sławomir Cielepak, dont la vision et l'expertise technique ont été déterminantes dans la création de cette solution. Nous sommes également reconnaissants envers Guy Burroughes et Gary Cross pour leur soutien inestimable tout au long du processus de développement.
Nous vous invitons à découvrir la transformation par vous-même.
Découvrez nos notre dépôt GitHub et documentation rtest pour commencer à écrire des tests plus fiables pour vos applications robotiques. Nous accueillons les contributions de la communauté pour étendre et améliorer rtest.
Rejoignez-nous pour faire des tests fiables une pratique standard dans l'écosystème ROS 2 !
FAQ : développement ROS 2
ROS 2 is built for distributed, asynchronous systems. Messages pass through multiple layers, including middleware such as DDS, which introduces timing variability, race conditions, and nondeterministic behaviour. As a result, the same test can pass once and fail the next time without any code changes, especially in CI environments.
Flaky tests are usually caused by timing issues, service discovery delays, multithreading, middleware communication behaviour, and differences between local and CI environments. Since ROS 2 was not designed for isolated unit testing by default, controlling these variables is difficult without additional tooling.
Common workarounds include adding delays, polling for conditions, or tuning middleware configurations. While these techniques may temporarily reduce failures, they increase test complexity and execution time without eliminating unpredictability. They also do not scale well across larger projects.
Rtest intercepts communication at the rclcpp layer, which sits between user code and the lower-level ROS 2 infrastructure. Instead of allowing publishers, subscribers, services, and timers to connect to the full ROS 2 stack, rtest replaces them with controlled mock implementations during test builds. This approach maintains API compatibility while giving full control over message flow and execution timing.
No. Your production code remains completely unchanged. The substitution happens at compile time during test builds. Your node continues to call standard ROS 2 APIs such as create_publisher() or create_service(), but in tests, these return mock-enabled components.
Rtest provides mock implementations for all core communication components, including publishers, subscribers, services, timers, and clocks. This makes it possible to test both simple and complex interaction scenarios without relying on actual middleware communication.
arrow_circle_rightContactez-nous
Discutons de la manière dont nous pouvons faire avancer votre projet
arrow_circle_right Nos articles