├── 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 | --------------------------------------------------------------------------------