Python コードのユニットテスト

2016年1月26日

はじめに

Python コードのユニットテストについて。

環境

  • Windows 7 64 bit
  • MSYS2 64 bit (MinGW w64)
  • Anaconda for Windows (Python 2.7)

※MinGW にも Python が入っているので、Anaconda のほうを使うようにパスを設定している。

例として、以下のようなコードを考える。

func.py

def abs(x):
    if x >= 0:
        return x
    else:
        return -x

def positive(x):
    if x > 0:
        return True
    else:
        return False

テストを書く

テストは以下のように書く。

func_test.py

from unittest import TestCase, main
from func import *


class FuncTest(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_abs(self):
        self.assertEqual(abs(1), 1)
        self.assertEqual(abs(-1), 1)

    def test_positive(self):
        self.assertTrue(positive(1))
        self.assertTrue(not positive(-1))


if __name__ == '__main__':
    main()

TestCase を継承したクラスを作り、その中にテストを書く。setUp() と tearDown() はそれぞれテスト前とテスト後に実行される関数である。assertEqual() は値が等しいことをチェックする関数で、assertTrue() は値が真であることをチェックする関数である。

テストの実行

テストを実行するには、次のようにする。

$ python -m unittest -v func_test.FuncTest
test_abs (func_test.FuncTest) ... ok
test_positive (func_test.FuncTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

オプション "-v" は詳細表示の指定である。

テストに失敗すると、次のようになる。

$ python -m unittest -v func_test.FuncTest
test_abs (func_test.FuncTest) ... FAIL
test_positive (func_test.FuncTest) ... ok

======================================================================
FAIL: test_abs (func_test.FuncTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "func_test.py", line 14, in test_abs
    self.assertEqual(abs(-1), 1)
AssertionError: -1 != 1

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

カレントディレクトリにあるテストコードを含むファイルをすべて実行させることもできる。テストコードを含むファイル名を "*_test.py" とすると、以下のように実行する。

$ $ python -m unittest discover -p "*_test.py" -v

オプション "-p" でテストコードのファイル名のパターンを指定している。

nose の利用

nose というテストツールを使うこともできる。

上のテストを次のように書ける。

from unittest import TestCase
from nose.tools import eq_, ok_
from func import *


class FuncTest(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_abs(self):
        eq_(abs(1), 1)
        eq_(abs(-1), 1)

    def test_positive(self):
        ok_(positive(1))
        ok_(not positive(-1))

eq_() は assertEqual() の代り、ok_() は assertTrue() の代りである。

テストは次のように実行する。

$ nosetests -v func_test.py
test_abs (func_test.FuncTest) ... ok
test_positive (func_test.FuncTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

オプション "-v" は詳細表示の指定である。nosetests はテストの標準出力を出力させないため、もし出力させたい場合はオプション "-s" を指定する。nosetests は、ファイル名を指定しなければ、カレントディレクトリにある名前からしてテストコードと思われるファイルの中からテストを探して実行してくれる。

テストが失敗した時点でテストを停止したい場合は、オプションで "-x" を指定する。

$ nosetests -v -x
test_abs (func_test.FuncTest) ... FAIL

======================================================================
FAIL: test_abs (func_test.FuncTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\msys64\home\yuyu\unittest\nose\func_test.py", line 15, in test_abs
    eq_(abs(-1), 1)
AssertionError: -1 != 1

----------------------------------------------------------------------
Ran 1 test in 0.006s

FAILED (failures=1)

コードカバレッジの計測

Coverage があれば、コードカバレッジを測ることができる。

Coverage のインストールは次のようにする。

$ pip install coverage

コードカバレッジを計測するには、テストを次のように実行する。

$ coverage run func_test.py

計測結果は次のように得られる。

$ coverage report
Name           Stmts   Miss  Cover
----------------------------------
func.py            8      0   100%
func_test.py      15      0   100%
----------------------------------
TOTAL             23      0   100%

もしテストの範囲が十分でないと、次のようになる。

$ coverage report
Name           Stmts   Miss  Cover
----------------------------------
func.py            8      2    75%
func_test.py      13      0   100%
----------------------------------
TOTAL             21      2    90%

次のようにすると、HTML を生成してくれる。

$ coverage html

htmlconv/index.html をブラウザで開く。この情報を見ると、下図のように、どこを通り損ねているか一目瞭然である。

Coverage の情報を初期化したい場合は、次のようにする。

$ coverage erase

nose の場合

nosetests のオプションで "--with-coverage" を指定すると、Coverage を実行してくれる。

$ nosetests -v --with-coverage
test_abs (func_test.FuncTest) ... ok
test_positive (func_test.FuncTest) ... ok

Name      Stmts   Miss  Cover   Missing
---------------------------------------
func.py       8      0   100%
----------------------------------------------------------------------
Ran 2 tests in 0.009s

OK

オプション "--cover-html" で HTML 生成を、"--cover-erase" で実行前に erase を実行してくれる。

参考