Python3のアプリをNoseとCoverageを使ってユニットテストします。Nose は unittest に基づいて記述したテストケースの他、独自の命名規則に基づくファイル/モジュールの関数/クラス/メソッドをテストケースとして実行できます。Coverageはユニットテストがテスト対象のコードをどれだけ試験したかを、コード全体に対する割合で表したもので、ユニットテストの作業状況が把握できます。「nose」に詳細なNoseの仕様を示します。また、「coverage」に詳細なCoverageの仕様を示します。
NoseとCoverageのインストール
Python3のアプリをユニットテストするために、今回はNoseとCoverageをRaspberry Pi 3にインストールします。次のコマンドでNoseをインストールします。
$ sudo apt-get install python3-nose
次のコマンドでCoverageをインストールします。
$ sudo apt-get install python3-coverage
テストされるアプリ
ユニットテストで使用するアプリは、「Pythonで非同期処理(asyncio)- イベントオブジェクト」で使用したアプリを次のように変更して使用します。
- モジュール名称を変更する。”test”の文字列が入るとテストスクリプトとみなされる。
- masync.pyの起動実行スクリプトをmain関数としてまとめる。
作成したPython3のアプリ「masync.py」「masync1.py」を次に示します。
lib/masync.py
import logging.config import asyncio import masync import masync1 import sys import warnings logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(filename)s:%(lineno)d %(message)s', stream=sys.stderr, ) LOG = logging.getLogger() def stop_operation(future): LOG.info("stop_operation") if not future.cancelled(): LOG.info(future.result()) async def operation(loop): queue = asyncio.Queue() LOG.info('start operation') task = asyncio.ensure_future(masync1.operationsub(queue)) LOG.info('operation1') await asyncio.sleep(0.2) queue.put_nowait(["start", 1,2,3]) LOG.info('operation2') await asyncio.sleep(0.2) queue.put_nowait(["next", 4,5,6]) LOG.info('operation3') await asyncio.sleep(0.2) queue.put_nowait(["stop", 7,8,9]) await asyncio.sleep(3) LOG.info('operation4') loop.stop() def main(): LOG.info('start') loop = asyncio.get_event_loop() LOG.info('enabling debugging') # Enable debugging # loop.set_debug(True) # Make the threshold for "slow" tasks very very small for # illustration. The default is 0.1, or 100 milliseconds. loop.slow_callback_duration = 0.001 # Report all mistakes managing asynchronous resources. warnings.simplefilter('always', ResourceWarning) LOG.info('entering event loop') asyncio.ensure_future(operation(loop)) loop.run_forever() loop.close() exit() if __name__ == '__main__': main()
lib/masync1.py
import logging.config import asyncio import sys async def operationsub(queue): logging.config.fileConfig('logging.conf') LOG = logging.getLogger() LOG.info('start operationsub') LOG.info('operationsub10') while True: if not queue.empty(): queuedata = await queue.get() LOG.info('queuedata:{}'.format(queuedata)) if queuedata[0]=="stop": break await asyncio.sleep(0.2) LOG.info('operationsub11')
ユニットテスト環境とスクリプト
ユニットテスト時のディレクトリ構成
Nose は、ユニットテスト実行時のカレントディレクトリから、Nose のテストケースの命名規則に基づき再帰的にテストケースを探し、実行してくれます。モジュール名(ディレクトリ名)、ファイル名、関数名、クラス名、メソッド名に ”test” という単語が含まれて入ればテストケースとして認識されます。今回は最上位のディレクトリでNoseを実行します。ホルダ「cover」には、Coverageで計測した結果がHTML形式で編集した結果が保存されます。ホルダ「lib」には、ユニットテストで使用するアプリ「masync.py」「masync1.py」を保存します。ホルダ「log」には、loggingモジュールによるロギング結果を保存します。ホルダ「test_myfunc」には、ユニットテストに使用するスクリプト「async_test.py」を保存します。このホルダの名称に”test”が入っているので、Noseは、ユニットテストに使用するスクリプトが設定されているホルダと認識します。
C:. │ .noserc │ logging.conf │ ├─cover │ coverage_html.js │ index.html │ jquery.ba-throttle-debounce.min.js │ jquery.hotkeys.js │ jquery.isonscreen.js │ jquery.min.js │ jquery.tablesorter.min.js │ keybd_closed.png │ keybd_open.png │ masync1_py.html │ masync_py.html │ status.json │ style.css │ ├─lib │ masync.py │ masync1.py ├─log │ test.log │ └─test_myfunc async_test.py
Noseの設定ファイル
Coverageで計測するときは「–with-coverage」オプションを付けます。「–cover-package」オプションで測定対象のパッケージ「masync」「masync1」を指定します。今回は、これらのオプションをまとめた設定ファイル「.noserc」を用意し、設定ファイルとして「.noserc」を参照するように「nosetests -c .noserc」で呼び出します。
.noserc
[nosetests] with-coverage=1 cover-package=masync,masync1 cover-branches=1 cover-html=1 verbosity=2
ユニットテストに使用するスクリプト
ユニットテストに使用するスクリプトでは、TestCase を継承したクラス「async_test.py」を作り、その中にテストを書きます。setUp() と tearDown() はそれぞれテスト前とテスト後に実行される関数です。そのほか、Noseでは、値が等しいことをチェックする関数「eq_」や、値が真であることをチェックする関数「ok_」が使用できます。
test_myfunc/async_test.py
from unittest import TestCase from nose.tools import eq_, ok_ from masync import * import asyncio class MainTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_main(self): main()
ユニットテストの実行
用意したユニットテスト「test_myfunc/async_test.py」を 次のnosetests コマンドで実行します。実行した結果を見ると次のことがわかります。
- モジュール「masync.py」の600行目でexit関数を呼び出しているのでERRORが発生しました。
- 捕捉されたロギング情報が表示されます。
- ユニットテストの結果がCoverageで表示されます。「masync.py」が84%、「masync1.py」が100%になっています。
$ nosetests3 -c .noserc test_main (async_test.MainTest) ... ERROR ====================================================================== ERROR: test_main (async_test.MainTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/pi/test_nose/test_myfunc/async_test.py", line 15, in test_main main() File "/home/pi/test_nose/lib/masync.py", line 60, in main exit() File "/usr/lib/python3.5/_sitebuiltins.py", line 26, in __call__ raise SystemExit(code) SystemExit: None -------------------- >> begin captured stdout << --------------------- 2018-01-13 18:41:41,524 masync1.py:8 start operationsub 2018-01-13 18:41:41,525 masync1.py:9 operationsub10 2018-01-13 18:41:41,717 masync.py:28 operation2 2018-01-13 18:41:41,727 masync1.py:13 queuedata:['start', 1, 2, 3] 2018-01-13 18:41:41,919 masync.py:31 operation3 2018-01-13 18:41:41,929 masync1.py:13 queuedata:['next', 4, 5, 6] 2018-01-13 18:41:42,131 masync1.py:13 queuedata:['stop', 7, 8, 9] 2018-01-13 18:41:42,133 masync1.py:18 operationsub11 2018-01-13 18:41:45,126 masync.py:35 operation4 --------------------- >> end captured stdout << ---------------------- -------------------- >> begin captured logging << -------------------- root: INFO: start asyncio: DEBUG: Using selector: EpollSelector root: INFO: enabling debugging root: INFO: entering event loop root: INFO: start operation root: INFO: operation1 --------------------- >> end captured logging << --------------------- Name Stmts Miss Branch BrPart Cover ---------------------------------------------- masync.py 41 4 4 1 84% masync1.py 16 0 4 0 100% ---------------------------------------------- TOTAL 57 4 8 1 89% ---------------------------------------------------------------------- Ran 1 test in 3.816s FAILED (errors=1)
Coverageはフォルダ「cover」に作成されます。「index.html」をクリックすると、各モジュールに対して次のようなカバレッジレポートが表示されます。
表示されている「masync.py」をクリックすると次のように「masync.py」モジュールのカバレッジが表示されます。テストされていない箇所は赤く表示されます。
表示されている「masync1.py」をクリックすると次のように「masync1.py」モジュールのカバレッジが表示されます。