TL;DR

Python用のROSパッケージで単体テストを書き,industrial_ci.travis.yml を使ってTravisを通すには .travis.yml の変数に CATKIN_CONFIG="--no-install を追加しないといけない.

うまくいったときの様子(rostestnosetests).

キーワード

  • Python
  • rostest
  • Travis CI, industrial_ci
  • unittest
  • nosetests
  • Ubuntu 16.04, ROS Kinetic
  • Ubuntu 18.04, ROS Melodic

bagmetti という,bagファイルのメッセージをフィルタしたり中身を変更したりする便利なパッケージを以前作りました.これに単体テストを書きたい.ついでに,ローカル環境はROS MelodicなのでKineticでもビルドできることが確認できるといいなーと思い,Travisを使います(とはいってもPythonなのでコンパイルは不要だし,外部ライブラリに依存していないので問題ないはずなんですが).

rostestを使った単体テストの実装

ROSは,Pythonの単体テストに関するドキュメントが非常に残念であることに気がつきました.公式のWikiが5年以上前の情報だったり,そもそもstackoverflowとかであまりヒットしない.

とはいうものの,Pythonのunittestのドキュメントは充実しています.rostestを使うのが良さそうだったので,テストケースを軽快に書いていきます.ローカル環境で rostest を実行してpassしたので,これでCIに投げて一段落!と思いきや……

テストが通らない!

industrial_ci というROSのためのCIテンプレートがあったので,それをベースに .travis.yml を書いていきます.で,早速pushしてみたところ,見事にエラーが!

まぁCIでいきなりうまくいくことはないかと思いつつ,修正を加えるのですが,なかなか通らない.どうもテストコードで import している自前のモジュールが見つかっていないらしい.

======================================================================
ERROR: Failure: ImportError (No module named bagmetti.rules.rename)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/nose/loader.py", line 418, in loadTestsFromName
    addr.filename, addr.module)
  File "/usr/lib/python2.7/dist-packages/nose/importer.py", line 47, in importFromPath
    return self.importFromDir(dir_path, fqname)
  File "/usr/lib/python2.7/dist-packages/nose/importer.py", line 94, in importFromDir
    mod = load_module(part_fqname, fh, filename, desc)
  File "/home/naoki/ros/workspaces/melodic/tmp/src/bagmetti/test/test_rename.py", line 8, in <module>
    from bagmetti.rules.rename import RenameRule
ImportError: No module named bagmetti.rules.rename

たしかにローカル環境でも,一旦 builddevel を消してから catkin run_tests を実行すると,これが起こる.

どうもこの問題は既知らしく,wikiで触れられていました.

One example is a rostest being run as part of the tests. If the test itself depends on Python code which is e.g. being generated the path containing the generated code is not necessarily on the PYTHONPATH. catkin_make does not provide a mechanism to work around this problem

じゃあどうすれば良いかというと,ビルドと実行を分けてやりなさいということでした. industrial_ci は,ちゃんとそうしている模様なので問題なし.

それなのになぜモジュールが見つからないのかとハマりまくった結果,最終的に catkin の設定で Install Location が指定されていることが分かりました.インストールされる場所が想定と違うので,見つからなかったのか〜と半分働いていない頭で考えているのが現状.

問題の対策

対策は,単純に CATKIN_CONFIG 変数に --no-install を渡してやるというものでした.だいぶ無駄な時間を費してしまったので,だれかの役に立つことを願っています.

nosetestsを使った単体テスト

ああでもないこうでもないと試行錯誤している過程で,nosetestsを使ったらできるんじゃないかと思い,nosetestsでもやってみました.

rostestがROS独自の配信や購読といった仕組みを念頭に置いたテストができるようになっているのに対し,noseは他ノードとやりとりしないようなスクリプトの単体テストに向いています.bagmettiは後者の使い方が多いので,noseでも良いかなーと思った次第です.

結論としては,nosetestsでもrostestでも, --no-install さえ渡してやればどちらでもテストは動くということが分かりました.でも,nosetestsのほうがそっけないです.

nosetests

........
----------------------------------------------------------------------
Ran 8 tests in 0.170s

OK

rostest

[ROSTEST]-----------------------------------------------------------------------


SUMMARY
 * RESULT: SUCCESS
 * TESTS: 0
 * ERRORS: 0
 * FAILURES: 0

rostest log file is in /home/naoki/.ros/log/rostest-nazousagi-1694.log
[Testcase: testtest_filter] ... ok

[ROSTEST]-----------------------------------------------------------------------

[bagmetti.rosunit-test_filter/test_is_exclude][passed]
[bagmetti.rosunit-test_filter/test_is_include][passed]
[bagmetti.rosunit-test_filter/test_is_tf][passed]
[bagmetti.rosunit-test_filter/test_is_time][passed]
[bagmetti.rosunit-test_filter/test_is_topic][passed]
[bagmetti.rosunit-test_filter/test_match_tf][passed]
[bagmetti.rosunit-test_filter/test_match_time][passed]
[bagmetti.rosunit-test_filter/test_match_topic][passed]

SUMMARY
 * RESULT: SUCCESS
 * TESTS: 8
 * ERRORS: 0
 * FAILURES: 0

無事,テストが通るようになりました.みなさんもぜひ楽しいTravis + ROS + ユニットテスト生活を!