├── src
└── my_pkg
│ ├── __init__.py
│ ├── hello.py
│ └── tests
│ └── test_hello.py
├── tests
├── pytest.ini
├── hello
│ └── test_hello.py
├── lib_test.launch
├── hello_test.launch
├── listener_test.launch
├── pytest_runner.py
└── listener
│ └── test_listener.py
├── .gitignore
├── bin
├── hello
└── publisher
├── README.md
├── setup.py
├── package.xml
└── CMakeLists.txt
/src/my_pkg/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | junit_suite_name = my_pkg_tests
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | *.pyc
3 |
4 | # pytest
5 | .coverage
6 | .cache
7 | .pytest_cache
--------------------------------------------------------------------------------
/bin/hello:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import my_pkg.hello
3 |
4 |
5 | if __name__ == '__main__':
6 | my_pkg.hello.say('my friend!')
7 |
--------------------------------------------------------------------------------
/tests/hello/test_hello.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | def test_hello_works():
3 | assert True
4 |
5 |
6 | def test_hello_does_not_work():
7 | assert not False
8 |
--------------------------------------------------------------------------------
/tests/lib_test.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/my_pkg/hello.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | def _produce_message(name):
3 | return 'Hello {}'.format(name)
4 |
5 |
6 | def say(name):
7 | print(_produce_message(name))
8 |
--------------------------------------------------------------------------------
/tests/hello_test.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/my_pkg/tests/test_hello.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from my_pkg import hello
3 |
4 |
5 | def test_hello_produces_friendly_message():
6 | message = hello._produce_message('Joe')
7 |
8 | assert message == 'Hello Joe'
9 |
--------------------------------------------------------------------------------
/tests/listener_test.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pytest + ROS Node Example
2 |
3 | Example package demonstrates how to use pytest for testing ROS nodes.
4 |
5 | You can run the tests with:
6 |
7 | ```bash
8 | catkin run_tests --this
9 | ```
10 |
11 | For info please read my [blog post on machinekoder.com](http://machinekoder.com/testing-ros-powered-robots-pytest/).
12 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD
2 |
3 | from distutils.core import setup
4 | from catkin_pkg.python_setup import generate_distutils_setup
5 |
6 | # fetch values from package.xml
7 | setup_args = generate_distutils_setup(
8 | packages=['my_pkg'],
9 | package_dir={'': 'src'},
10 | )
11 |
12 | setup(**setup_args)
13 |
--------------------------------------------------------------------------------
/bin/publisher:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import rospy
4 | from std_msgs.msg import String
5 |
6 |
7 | def talker():
8 | rospy.init_node('talker', anonymous=True)
9 | pub = rospy.Publisher('chatter', String, queue_size=10)
10 | rate = rospy.Rate(10) # 10hz
11 | while not rospy.is_shutdown():
12 | hello_str = "hello world %s" % rospy.get_time()
13 | rospy.loginfo(hello_str)
14 | pub.publish(hello_str)
15 | rate.sleep()
16 |
17 |
18 | if __name__ == '__main__':
19 | try:
20 | talker()
21 | except rospy.ROSInterruptException:
22 | pass
23 |
--------------------------------------------------------------------------------
/tests/pytest_runner.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import print_function
3 |
4 | import os
5 | import sys
6 | import rospy
7 | import pytest
8 |
9 |
10 | def get_output_file():
11 | for arg in sys.argv:
12 | if arg.startswith('--gtest_output'):
13 | return arg.split('=xml:')[1]
14 |
15 | raise RuntimeError('No output file has been passed')
16 |
17 |
18 | if __name__ == '__main__':
19 | output_file = get_output_file()
20 | test_module = rospy.get_param('test_module')
21 | runner_path = os.path.dirname(os.path.realpath(__file__))
22 | module_path = os.path.join(runner_path, test_module)
23 |
24 | sys.exit(
25 | pytest.main([module_path, '--junitxml={}'.format(output_file)])
26 | )
27 |
--------------------------------------------------------------------------------
/tests/listener/test_listener.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import pytest
3 | import rospy
4 | import time
5 |
6 | from std_msgs.msg import String
7 |
8 | NAME = 'talker_listener_test'
9 |
10 |
11 | @pytest.fixture
12 | def node():
13 | rospy.init_node(NAME, anonymous=True)
14 |
15 |
16 | @pytest.fixture
17 | def waiter():
18 | class Waiter(object):
19 | def __init__(self):
20 | self.received = []
21 | self.condition = lambda x: False
22 |
23 | @property
24 | def success(self):
25 | return True in self.received
26 |
27 | def callback(self, data):
28 | self.received.append(self.condition(data))
29 |
30 | def wait(self, timeout):
31 | timeout_t = time.time() + timeout
32 | while not rospy.is_shutdown() and not self.success and time.time() < timeout_t:
33 | time.sleep(0.1)
34 |
35 | def reset(self):
36 | self.received = []
37 |
38 | return Waiter()
39 |
40 |
41 | def test_listener_receives_something(node, waiter):
42 | waiter.condition = lambda data: True # any message is good
43 |
44 | rospy.Subscriber('chatter', String, waiter.callback)
45 | waiter.wait(10.0)
46 |
47 | assert waiter.success
48 |
49 |
50 | def test_listener_receives_hello_mesage(node, waiter):
51 | waiter.condition = lambda data: 'hello world' in data.data
52 |
53 | rospy.Subscriber('chatter', String, waiter.callback)
54 | waiter.wait(10.0)
55 |
56 | assert waiter.success
57 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | my_pkg
4 | 0.0.0
5 | The my_pkg package
6 |
7 |
8 |
9 |
10 | alexander
11 |
12 |
13 |
14 |
15 |
16 | TODO
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | catkin
52 | message_generation
53 | rospy
54 | rospy
55 | rospy
56 | python-pytest
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 2.8.3)
2 | project(my_pkg)
3 |
4 | ## Compile as C++11, supported in ROS Kinetic and newer
5 | # add_compile_options(-std=c++11)
6 |
7 | ## Find catkin macros and libraries
8 | ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
9 | ## is used, also find other catkin packages
10 | find_package(catkin REQUIRED COMPONENTS
11 | message_generation
12 | rospy
13 | )
14 |
15 | ## System dependencies are found with CMake's conventions
16 | # find_package(Boost REQUIRED COMPONENTS system)
17 |
18 |
19 | ## Uncomment this if the package has a setup.py. This macro ensures
20 | ## modules and global scripts declared therein get installed
21 | ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html
22 | catkin_python_setup()
23 |
24 | ################################################
25 | ## Declare ROS messages, services and actions ##
26 | ################################################
27 |
28 | ## To declare and build messages, services or actions from within this
29 | ## package, follow these steps:
30 | ## * Let MSG_DEP_SET be the set of packages whose message types you use in
31 | ## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...).
32 | ## * In the file package.xml:
33 | ## * add a build_depend tag for "message_generation"
34 | ## * add a build_depend and a run_depend tag for each package in MSG_DEP_SET
35 | ## * If MSG_DEP_SET isn't empty the following dependency has been pulled in
36 | ## but can be declared for certainty nonetheless:
37 | ## * add a run_depend tag for "message_runtime"
38 | ## * In this file (CMakeLists.txt):
39 | ## * add "message_generation" and every package in MSG_DEP_SET to
40 | ## find_package(catkin REQUIRED COMPONENTS ...)
41 | ## * add "message_runtime" and every package in MSG_DEP_SET to
42 | ## catkin_package(CATKIN_DEPENDS ...)
43 | ## * uncomment the add_*_files sections below as needed
44 | ## and list every .msg/.srv/.action file to be processed
45 | ## * uncomment the generate_messages entry below
46 | ## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...)
47 |
48 | ## Generate messages in the 'msg' folder
49 | # add_message_files(
50 | # FILES
51 | # Message1.msg
52 | # Message2.msg
53 | # )
54 |
55 | ## Generate services in the 'srv' folder
56 | # add_service_files(
57 | # FILES
58 | # Service1.srv
59 | # Service2.srv
60 | # )
61 |
62 | ## Generate actions in the 'action' folder
63 | # add_action_files(
64 | # FILES
65 | # Action1.action
66 | # Action2.action
67 | # )
68 |
69 | ## Generate added messages and services with any dependencies listed here
70 | # generate_messages(
71 | # DEPENDENCIES
72 | # std_msgs # Or other packages containing msgs
73 | # )
74 |
75 | ################################################
76 | ## Declare ROS dynamic reconfigure parameters ##
77 | ################################################
78 |
79 | ## To declare and build dynamic reconfigure parameters within this
80 | ## package, follow these steps:
81 | ## * In the file package.xml:
82 | ## * add a build_depend and a run_depend tag for "dynamic_reconfigure"
83 | ## * In this file (CMakeLists.txt):
84 | ## * add "dynamic_reconfigure" to
85 | ## find_package(catkin REQUIRED COMPONENTS ...)
86 | ## * uncomment the "generate_dynamic_reconfigure_options" section below
87 | ## and list every .cfg file to be processed
88 |
89 | ## Generate dynamic reconfigure parameters in the 'cfg' folder
90 | # generate_dynamic_reconfigure_options(
91 | # cfg/DynReconf1.cfg
92 | # cfg/DynReconf2.cfg
93 | # )
94 |
95 | ###################################
96 | ## catkin specific configuration ##
97 | ###################################
98 | ## The catkin_package macro generates cmake config files for your package
99 | ## Declare things to be passed to dependent projects
100 | ## INCLUDE_DIRS: uncomment this if your package contains header files
101 | ## LIBRARIES: libraries you create in this project that dependent projects also need
102 | ## CATKIN_DEPENDS: catkin_packages dependent projects also need
103 | ## DEPENDS: system dependencies of this project that dependent projects also need
104 | catkin_package(
105 | # INCLUDE_DIRS include
106 | # LIBRARIES my_pkg
107 | # CATKIN_DEPENDS message_generation rospy
108 | # DEPENDS system_lib
109 | )
110 |
111 | ###########
112 | ## Build ##
113 | ###########
114 |
115 | ## Specify additional locations of header files
116 | ## Your package locations should be listed before other locations
117 | include_directories(
118 | # include
119 | ${catkin_INCLUDE_DIRS}
120 | )
121 |
122 | ## Declare a C++ library
123 | # add_library(${PROJECT_NAME}
124 | # src/${PROJECT_NAME}/my_pkg.cpp
125 | # )
126 |
127 | ## Add cmake target dependencies of the library
128 | ## as an example, code may need to be generated before libraries
129 | ## either from message generation or dynamic reconfigure
130 | # add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
131 |
132 | ## Declare a C++ executable
133 | ## With catkin_make all packages are built within a single CMake context
134 | ## The recommended prefix ensures that target names across packages don't collide
135 | # add_executable(${PROJECT_NAME}_node src/my_pkg_node.cpp)
136 |
137 | ## Rename C++ executable without prefix
138 | ## The above recommended prefix causes long target names, the following renames the
139 | ## target back to the shorter version for ease of user use
140 | ## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node"
141 | # set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "")
142 |
143 | ## Add cmake target dependencies of the executable
144 | ## same as for the library above
145 | # add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
146 |
147 | ## Specify libraries to link a library or executable target against
148 | # target_link_libraries(${PROJECT_NAME}_node
149 | # ${catkin_LIBRARIES}
150 | # )
151 |
152 | #############
153 | ## Install ##
154 | #############
155 |
156 | # all install targets should use catkin DESTINATION variables
157 | # See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html
158 |
159 | ## Mark executable scripts (Python etc.) for installation
160 | ## in contrast to setup.py, you can choose the destination
161 | # install(PROGRAMS
162 | # scripts/my_python_script
163 | # DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
164 | # )
165 |
166 | ## Mark executables and/or libraries for installation
167 | # install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_node
168 | # ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
169 | # LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
170 | # RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
171 | # )
172 |
173 | ## Mark cpp header files for installation
174 | # install(DIRECTORY include/${PROJECT_NAME}/
175 | # DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
176 | # FILES_MATCHING PATTERN "*.h"
177 | # PATTERN ".svn" EXCLUDE
178 | # )
179 |
180 | ## Mark other files for installation (e.g. launch and bag files, etc.)
181 | # install(FILES
182 | # # myfile1
183 | # # myfile2
184 | # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
185 | # )
186 |
187 | catkin_install_python(PROGRAMS
188 | bin/hello
189 | bin/publisher
190 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
191 |
192 | #############
193 | ## Testing ##
194 | #############
195 |
196 | ## Add gtest based cpp test target and link libraries
197 | # catkin_add_gtest(${PROJECT_NAME}-test test/test_my_pkg.cpp)
198 | # if(TARGET ${PROJECT_NAME}-test)
199 | # target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME})
200 | # endif()
201 |
202 | ## Add folders to be run by python nosetests
203 | # catkin_add_nosetests(test)
204 |
205 | if(CATKIN_ENABLE_TESTING)
206 | find_package(rostest REQUIRED)
207 | add_rostest(tests/hello_test.launch)
208 | add_rostest(tests/listener_test.launch)
209 | add_rostest(tests/lib_test.launch)
210 | endif()
211 |
--------------------------------------------------------------------------------