{"id":15672620,"url":"https://github.com/felixdivo/ros2-easy-test","last_synced_at":"2025-04-14T09:42:28.213Z","repository":{"id":152380125,"uuid":"615878648","full_name":"felixdivo/ros2-easy-test","owner":"felixdivo","description":"A Python test framework for ROS2 allowing simple and expressive assertions based on message interactions.","archived":false,"fork":false,"pushed_at":"2025-01-27T22:56:06.000Z","size":187,"stargazers_count":20,"open_issues_count":4,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-09T12:38:44.768Z","etag":null,"topics":["nodes","ros","ros2","testing","testing-library"],"latest_commit_sha":null,"homepage":"https://ros2-easy-test.readthedocs.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/felixdivo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-18T23:53:56.000Z","updated_at":"2025-03-28T08:14:16.000Z","dependencies_parsed_at":"2024-03-09T00:32:54.983Z","dependency_job_id":"0fea61bf-107c-411d-89c0-6269c9e7d01c","html_url":"https://github.com/felixdivo/ros2-easy-test","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixdivo%2Fros2-easy-test","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixdivo%2Fros2-easy-test/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixdivo%2Fros2-easy-test/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixdivo%2Fros2-easy-test/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/felixdivo","download_url":"https://codeload.github.com/felixdivo/ros2-easy-test/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248855961,"owners_count":21172673,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nodes","ros","ros2","testing","testing-library"],"created_at":"2024-10-03T15:28:58.229Z","updated_at":"2025-04-14T09:42:28.190Z","avatar_url":"https://github.com/felixdivo.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ROS2 easy-test\r\n\r\n[![license](https://img.shields.io/pypi/l/ros2-easy-test.svg?color=blue)](https://github.com/felixdivo/ros2-easy-test/blob/main/LICENSE)\r\n[![ros2 version](https://img.shields.io/badge/ROS2-Humble+%20(deprecated,%20via%20pip)-blue)](https://docs.ros.org/en/rolling/Releases.html)\r\n[![ros2 version](https://img.shields.io/badge/ROS2-Kilted+%20(via%20rosdep)-blue)](https://docs.ros.org/en/rolling/Releases.html)\r\n[![Python version](https://img.shields.io/badge/python-3.8+%20(matching%20ROS)-blue)](https://devguide.python.org/versions/)\r\n[![ROS Package Index](https://img.shields.io/ros/v/rolling/ros2_easy_test)](https://index.ros.org/p/ros2_easy_test)\r\n\r\n[![CI status](https://github.com/felixdivo/ros2-easy-test/actions/workflows/python-package.yaml/badge.svg)](https://github.com/felixdivo/ros2-easy-test/actions/workflows/python-package.yaml)\r\n[![documentation status](https://readthedocs.org/projects/ros2-easy-test/badge/)](https://ros2-easy-test.readthedocs.io/en/latest/)\r\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\r\n[![static type checker](https://img.shields.io/badge/static%20typing-mypy-black)](https://mypy-lang.org/)\r\n\r\nA Python test framework for [ROS2](https://ros.org/) allowing for:\r\n- simple and expressive assertions based on message and service interactions (black box testing)\r\n- easy integration of existing nodes and launch files\r\n- testing of nodes implemented in any programming language (C++, Python, etc.)\r\n- works with and without tools like `colcon test` and `pytest`\r\n- is minimalistic and has [very few dependencies](https://github.com/felixdivo/ros2-easy-test/blob/main/ros2_easy_test/package.xml)\r\n- is typed and [documented](https://ros2-easy-test.readthedocs.io/en/latest/)\r\n- is tested, used in practice, [indexed](https://index.ros.org/p/ros2_easy_test), and maintained\r\n\r\n## Installation\r\n\r\n**Note**: *The former [PyPI package `ros2-easy-test`](https://pypi.org/project/ros2-easy-test/) is now deprecated and will not be updated anymore. Thus, only use `pip install ros2-easy-test` for ROS versions prior to Kilted.*\r\n\r\nJust run (ROS2 `Rolling` and `Kilted+`):\r\n```shell\r\nrosdep install ros2_easy_test\r\n```\r\n\r\nOr add it to your `package.xml` (see the [ROS2 docs](https://docs.ros.org/en/rolling/Tutorials/Beginner-Client-Libraries/Creating-A-Workspace/Creating-A-Workspace.html#resolve-dependencies)):\r\n```xml\r\n\u003ctest_depend\u003eros2_easy_test\u003c/test_depend\u003e\r\n```\r\n\r\n## Examples\r\n\r\nThe following two examples demonstrate the usage of the Python decorators `@with_single_node` and `@with_launch_file`, which provide this package's core functionality.\r\nTo get a better understanding of their inner workings, please have a look at their implementation [here](ros2_easy_test/decorators.py).\r\nBesides the simple examples here, you can embed everything in `unittest.TestCase` as well. \r\nTo check out how, have a look at the provided test in [tests/demo/](tests/demo/) for some advanced examples.\r\n\r\n### Testing a Node\r\n\r\nIn simple settings, where a single node is to be tested, the decorator `@with_single_node` can be used:\r\n\r\n```python\r\nfrom ros2_easy_test import ROS2TestEnvironment, with_launch_file, with_single_node\r\nfrom my_nodes import Talker\r\nfrom std_msgs.msg import String\r\n\r\n@with_single_node(Talker, watch_topics={\"/chatter\": String})\r\ndef test_simple_publisher(env: ROS2TestEnvironment) -\u003e None:\r\n    response: String = env.assert_message_published(\"/chatter\", timeout=5)\r\n    assert response.data == \"Hello World: 0\"\r\n```\r\nIt is also possible to specify custom QoSProfiles for the `ROS2TestEnvironment` to use when subscribing or publishing on the specified topics. See [`tests/demo/latching_topic_test.py`](tests/demo/latching_topic_test.py) for an example. If no profile is specified, the default `QoSProfile(history=QoSHistoryPolicy.KEEP_ALL)` is used.\r\n\r\nYou can optionally provide more parameters to the test setting, i.e., additionally pass `parameters={\"some.thing\": 30.2}` to the decorator.\r\nThe argument of the test function receiving the `ROS2TestEnvironment` *must* be named `env`.\r\n\r\n### Testing a Launch File\r\n\r\nFor more complex scenarios involving multiple nodes using a launch file (both nodes and launch file being implemented in [any language supported by ROS2](https://docs.ros.org/en/rolling/How-To-Guides/Launch-file-different-formats.html)), the `@with_launch_file` decorator can be used.\r\n\r\n```python\r\n@with_launch_file(\r\n    \"example_launch_file.yaml\",\r\n    watch_topics={\"/some/interesting/response\": ColorRGBA},\r\n)\r\ndef test_simple_update_launch_file(env: ROS2TestEnvironment) -\u003e None:\r\n    env.publish(\"/topic/for/node_input\", ColorRGBA(r=0.5, g=0.2, b=0.9, a=1.0))\r\n    response_color = env.assert_message_published(\"/some/interesting/response\")\r\n    assert response_color.r == 0.5\r\n```\r\n\r\nYou can also pass the literal launch file contents as a `str` instead of a path like `\"example_launch_file.yaml\"`.\r\nThe argument of the test function receiving the `ROS2TestEnvironment` *must* be named `env`.\r\n\r\nNote that, however, this method is much slower than the one above. \r\nOne reason for this is the requirement of a fixed warm-up time for the nodes to be started. \r\nThis is because the test environment has to wait for the nodes to be ready before it can start listening for messages.\r\n\r\n## Usage\r\n\r\n### How you can interact with the node(s)\r\n\r\nUsing `ROS2TestEnvironment`, you can call:\r\n- `publish(topic: str, message: RosMessage) -\u003e None`\r\n- `listen_for_messages(topic: str, time_span: float) -\u003e List[RosMessage]`\r\n- `clear_messages(topic: str) -\u003e None` to forget all messages that have been received so far.\r\n- `call_service(name: str, request: Request, ...) -\u003e Response`\r\n- `send_action_goal(name: str, goal: Any, ...) -\u003e Tuple[ClientGoalHandle, List[FeedbackMsg]]`\r\n- `send_action_goal_and_wait_for_result(name: str, goal: Any, ...) -\u003e Tuple[List[FeedbackMsg], ResultMsg]`\r\n\r\nNote that a `ROS2TestEnvironment` is a normal [`rclpy.node.Node`](https://docs.ros2.org/latest/api/rclpy/api/node.html) and thus has all the methods of any other ROS2 node.\r\nSo feel free to offer a service with `env.create_service()` and cover more specific use cases.\r\nExtend as you please!\r\n\r\nIn addition, nothing stops you from using any other means of interacting with ROS2 that would work otherwise.\r\n\r\n### What you can test (recommended way of obtaining messages)\r\n\r\nUsing `ROS2TestEnvironment`, you can assert:\r\n- `assert_message_published(topic: str, timeout: Optional[float]) -\u003e RosMessage`\r\n- `assert_no_message_published(topic: str, timeout: Optional[float]) -\u003e None`\r\n- `assert_messages_published(topic: str, number: int, ...) -\u003e List[RosMessage]`\r\n\r\nGenerally, you can always test that no exceptions are thrown, e.g., when nodes are initialized (see limitations below).\r\n\r\n### Combining with other tools\r\n\r\nSome hints:\r\n- If you want to use [pytest markers](https://docs.pytest.org/en/7.1.x/how-to/mark.html) like `@pytest.mark.skipif(...)`, add that above (=before) the `@with_single_node(...)`/`@with_launch_file(...)` decorator and it will work just fine.\r\n- Similarly, you can seamlessly use other tools which annotate test functions, like [hypothesis](https://hypothesis.readthedocs.io/en/latest/) or [pytest fixtures](https://docs.pytest.org/en/6.2.x/fixture.html).\r\n  Generally, you have to be mindful of the order of the decorators here.\r\n  The `ROS2TestEnvironment` is always added as a keyword argument called `env` to the test function.\r\n  See `tests/demo/` for a few examples.\r\n\r\n## Limitations, Design, and Other Projects\r\n\r\nSee [the documentation on that](https://ros2-easy-test.readthedocs.io/en/latest/design_and_limits.html).\r\nPlease read it before suggesting major features or changes.\r\n\r\n## Contributing\r\n\r\nBasic installation is easiest with the provided `Dockerfile`.\r\nFor the last piece of setup, either open it in the provided [Devcontainer](https://code.visualstudio.com/docs/remote/containers) or maunally run `rosdep install --from-paths ros2_easy_test \u0026\u0026 colcon build --symlink-install \u0026\u0026 pip install -e './ros2_easy_test[dev]'` afterward.\r\n\r\nAfter this, you will have access to the configured formatter (`ruff format`) and linter (`ruff check`).\r\n\r\nYou can run the test with simply `pytest`. Coverage reports and timings will be printed on the command line, and a fresh line-by-line coverage report is in `htmlcov/index.html`.\r\n\r\nBuilding the documentation is simple, too:\r\n```shell\r\n# Install the required dependencies\r\npip install -e 'ros2_easy_test[doc]'\r\n\r\n# Build the documentation\r\ncd doc\r\nmake html\r\n# open build/html/index.html in your browser\r\n\r\n# You can also run a small webserver to serve the static files with\r\ncd build/html\r\npython -m http.server\r\n```\r\n\r\nA new version can be released by pusing a new tag starting with `v` to the repository.\r\nThis will trigger the CI to draft a release in GitHub that can then be filled with the changelog.\r\nMake sure to update the version number in `package.xml` and `ros2_easy_test/ros2_easy_test/__init__.py` before tagging.\r\nThen, a release via rosdep can be made.\r\n\r\n## Changelog\r\n\r\nSee [Releases](https://github.com/felixdivo/ros2-easy-test/releases).\r\n\r\n## License\r\n\r\nSee [LICENSE](LICENSE).\r\n\r\nInitially developed by [Felix Divo](https://github.com/felixdivo) at [*Sailing Team Darmstadt e. V.*](https://www.st-darmstadt.de/), a student group devoted to robotic sailing based in Darmstadt, Germany.\r\nThanks to [Simon Kohaut](https://github.com/simon-kohaut) for his kind and nuanced feedback, and all [the other awesome contributors](https://github.com/felixdivo/ros2-easy-test/graphs/contributors) for their help and support!\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelixdivo%2Fros2-easy-test","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffelixdivo%2Fros2-easy-test","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelixdivo%2Fros2-easy-test/lists"}