Python3のアプリをNoseとCoverageでユニットテスト

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」をクリックすると、各モジュールに対して次のようなカバレッジレポートが表示されます。
Coverage report

表示されている「masync.py」をクリックすると次のように「masync.py」モジュールのカバレッジが表示されます。テストされていない箇所は赤く表示されます。
Coverage for masync.py

表示されている「masync1.py」をクリックすると次のように「masync1.py」モジュールのカバレッジが表示されます。
Coverage for masync1.py