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」モジュールのカバレッジが表示されます。
