ROS系统中提供了测试框架,可以实现python/c++代码的单元测试,python和C++通过不同的方式实现,
之后的两篇文档分别详细介绍各自的实现步骤,以及测试结果和覆盖率的获取。
ROS系统中python代码测试介绍
关于测试代码的写法细节请参考官方wiki文档,http://wiki.ros.org/unittest,本文主要说明使用中的坑。
ROS中python代码的测试可以有两种实现方式一是节点级的集成测试,可以测试节点创建和消息收发的整个过程;二是代码级的单元测试,在测试用例中导入被测代码进行测试。
python代码测试中可能遇到的问题及优化修改
1、创建启动测试的roslaunch文件
rostest相关的roslaunch请参考 http://wiki.ros.org/roslaunch/XML/test
<launch> <node name="nodename" pkg="pkgname" type="被测文件的py"/> <test test-name="testnodename" pkg="pkgname" time-limit="500.0" type="测试文件py" args="--cov"/> </launch>
关注点1:
time-limit这个标签限制了用例运行的最长时间,如果用例耗时超过这个时间,那么用例会自动以超时失败结束,默认值是60s。如果用例较多运行时间长的话,需要
设置合理的值;
关注点2:
args 这个标签可以向测试程序中传入参数,--cov的作用是测试完成后生成覆盖率文件,下面详细介绍。
2、测试结果文件获取
参见上一篇介绍环境变量了文章,通过ROS_TEST_RESULTS_DIR这个变量可以修改测试结果输出的位置。
3、覆盖率统计
我使用的ros版本是indigo,这个版本中rostest对覆盖率统计的代码需要进一步优化。
优化点1:rostest.rosrun(package_name, test_name, test_case_class, sysargs=None)
根据wiki中对rostest.rosrun的描述,该函数应该有第五个参数coverage_packages,该参数表示待测试package list.
优化后的函数rostest.rosrun(package_name, test_name, test_case_class, sysargs=None, coverage_packages=None)
优化点2:rostest覆盖率统计完善
覆盖率统计需要首先a安装python代码覆盖率工具coverge,参考http://coverage.readthedocs.org/en/latest/
修改rostest.rosrun代码,使代码能够输出xml_report,为什么要输出xml报告呢,因为The report is compatible with Cobertura reports.
这一点很关键,在jenkins持续集成环境中需要这一步骤的工作。jenkins中的Cobertura插件可以解析xml_report文件,然后将python代码的详细覆盖率信息显示在用例的测试结果中。
/opt/ros/indigo/lib/python2.7/dist-packages/rostest/__init__.py 中函数的修改
def rosrun(package, test_name, test, sysargs=None, coverage_packages=None): """ Run a rostest/unittest-based integration test. @param package: name of package that test is in @type package: str @param test_name: name of test that is being run @type test_name: str @param test: test class @type test: unittest.TestCase @param sysargs: command-line args. If not specified, this defaults to sys.argv. rostest will look for the --text and --gtest_output parameters @type sysargs: list """ if sysargs is None: # lazy-init sys args import sys sysargs = sys.argv #parse sysargs result_file = None for arg in sysargs: if arg.startswith(XML_OUTPUT_FLAG): result_file = arg[len(XML_OUTPUT_FLAG):] text_mode = '--text' in sysargs coverage_mode = '--cov' in sysargs if coverage_mode: _start_coverage(coverage_packages) import unittest import rospy coverresult = os.getenv('ROS_TEST_RESULTS_DIR') + '/coverage/' suite = unittest.TestLoader().loadTestsFromTestCase(test) if text_mode: result = unittest.TextTestRunner(verbosity=2).run(suite) else: result = rosunit.create_xml_runner(package, test_name, result_file).run(suite) if coverage_mode: _stop_coverage(coverage_packages, coverresult) rosunit.print_unittest_summary(result) # shutdown any node resources in case test forgets to rospy.signal_shutdown('test complete') if not result.wasSuccessful(): import sys sys.exit(1)
def _stop_coverage(packages, html=None): """ @param packages: list of packages to generate coverage reports for @type packages: [str] @param html: (optional) if not None, directory to generate html report to @type html: str """ if _cov is None: return import sys, os try: _cov.stop() # accumulate results _cov.save() # - update our own .coverage-modules file list for # coverage-html tool. The reason we read and rewrite instead # of append is that this does a uniqueness check to keep the # file from growing unbounded if os.path.exists('.coverage-modules'): with open('.coverage-modules','r') as f: all_packages = set([x for x in f.read().split(' ') if x.strip()] + packages) else: all_packages = set(packages) with open('.coverage-modules','w') as f: f.write(' '.join(all_packages)+' ') try: # list of all modules for html report all_mods = [] # iterate over packages to generate per-package console reports for package in packages: pkg = __import__(package) m = [v for v in sys.modules.values() if v and v.__name__.startswith(package)] all_mods.extend(m) # generate overall report and per module analysis _cov.report(m, show_missing=0) for mod in m: res = _cov.analysis(mod) print(" %s: Missing lines: %s"%(res[0], res[3])) if html: print("="*80+" generating html coverage report to %s "%html+"="*80) _cov.html_report(all_mods, directory=html) _cov.xml_report(all_mods, outfile=html + 'cover.xml') except ImportError as e: print("WARNING: cannot import '%s', will not generate coverage report"%package, file=sys.stderr) except ImportError as e: print("""WARNING: cannot import python-coverage, coverage tests will not run. To install coverage, run 'easy_install coverage'""", file=sys.stderr)
/opt/ros/indigo/lib/python2.7/dist-packages/rosunit/pyunit.py 文件修改
def unitrun(package, test_name, test, sysargs=None, coverage_packages=None): """ Wrapper routine from running python unitttests with JUnit-compatible XML output. This is meant for unittests that do not not need a running ROS graph (i.e. offline tests only). This enables JUnit-compatible test reporting so that test results can be reported to higher-level tools. WARNING: unitrun() will trigger a sys.exit() on test failure in order to properly exit with an error code. This routine is meant to be used as a main() routine, not as a library. @param package: name of ROS package that is running the test @type package: str @param coverage_packages: list of Python package to compute coverage results for. Defaults to package @type coverage_packages: [str] @param sysargs: (optional) alternate sys.argv @type sysargs: [str] """ if sysargs is None: # lazy-init sys args import sys sysargs = sys.argv import unittest if coverage_packages is None: coverage_packages = [package] #parse sysargs result_file = None for arg in sysargs: if arg.startswith(XML_OUTPUT_FLAG): result_file = arg[len(XML_OUTPUT_FLAG):] text_mode = '--text' in sysargs coverage_mode = '--cov' in sysargs or '--covhtml' in sysargs if coverage_mode: start_coverage(coverage_packages) # create and run unittest suite with our xmllrunner wrapper suite = unittest.TestLoader().loadTestsFromTestCase(test) if text_mode: result = unittest.TextTestRunner(verbosity=2).run(suite) else: result = create_xml_runner(package, test_name, result_file).run(suite) if coverage_mode: #cov_html_dir = 'covhtml_test' if '--covhtml' in sysargs else None cov_html_dir = os.getenv('ROS_TEST_RESULTS_DIR') + '/coverage/' stop_coverage(coverage_packages, html=cov_html_dir) # test over, summarize results and exit appropriately print_unittest_summary(result) if not result.wasSuccessful(): import sys sys.exit(1)
def stop_coverage(packages, html=None): """ @param packages: list of packages to generate coverage reports for @type packages: [str] @param html: (optional) if not None, directory to generate html report to @type html: str """ if _cov is None: return import sys, os try: _cov.stop() # accumulate results _cov.save() # - update our own .coverage-modules file list for # coverage-html tool. The reason we read and rewrite instead # of append is that this does a uniqueness check to keep the # file from growing unbounded if os.path.exists('.coverage-modules'): with open('.coverage-modules','r') as f: all_packages = set([x for x in f.read().split(' ') if x.strip()] + packages) else: all_packages = set(packages) with open('.coverage-modules','w') as f: f.write(' '.join(all_packages)+' ') try: # list of all modules for html report all_mods = [] # iterate over packages to generate per-package console reports for package in packages: pkg = __import__(package) m = [v for v in sys.modules.values() if v and v.__name__.startswith(package)] all_mods.extend(m) # generate overall report and per module analysis _cov.report(m, show_missing=0) for mod in m: res = _cov.analysis(mod) print(" %s: Missing lines: %s"%(res[0], res[3])) if html: print("="*80+" generating html coverage report to %s "%html+"="*80) _cov.html_report(all_mods, directory=html) _cov.xml_report(all_mods, outfile=html + 'cover.xml') except ImportError as e: print("WARNING: cannot import '%s', will not generate coverage report"%package, file=sys.stderr) except ImportError as e: print("""WARNING: cannot import python-coverage, coverage tests will not run. To install coverage, run 'easy_install coverage'""", file=sys.stderr)
4、测试代码例子
if __name__ == '__main__': for arg in sys.argv: print arg testfiles = [] testfiles.append('被测试的python package') testfiles.append('被测试的python package') rostest.rosrun(PKG, NAME, 测试类名, sys.argv, testfiles)