Python School 2.0.0 documentation

標準モジュールを使ったテスト

«  プロジェクトの構成   ::   Contents   ::   Nose を使ったテスト  »

標準モジュールを使ったテスト

Python の標準モジュールには有名なテスト実行方法が2つあります。 どちらも Doug Hellmann の PyMOTW で説明されています。 (英語)

unittestxUnit の Python 実装です。 doctest はコメントに記述されたコード片をテストしてくれるモジュールで、Python 独特の機能です。

unittest の実行方法

まずはテスト対象となるスクリプトを記述します。 フィボナッチ数を求めてみましょう。

fibonacci.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def fib(n):
    assert type(n) == int
    if n in (0, 1):
        return n
    else:
        return fib(n - 1) + fib(n - 2)


if __name__ == '__main__':
    for i in range(13):
        print("{:2}\t--fib-->\t{:3}".format(i, fib(i)))

# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :

実行結果

$ python fibonacci.py
 0      --fib-->          0
 1      --fib-->          1
 2      --fib-->          1
 3      --fib-->          2
 4      --fib-->          3
 5      --fib-->          5
 6      --fib-->          8
 7      --fib-->         13
 8      --fib-->         21
 9      --fib-->         34
10      --fib-->         55
11      --fib-->         89
12      --fib-->        144

これに対するテストスクリプトを fibonacci_test.py として作成します。 スクリプトを直接実行したときにテストが実行されるように、 unittest.main() を記述しておきます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

# Local script to test.
from fibonacci import fib


class FibonacciTest(unittest.TestCase):

    def test(self):
        self.assertEqual(0, fib(0))
        self.assertEqual(1, fib(1))
        self.assertEqual(1, fib(2))
        self.assertEqual(2, fib(3))
        self.assertEqual(3, fib(4))
        self.assertEqual(5, fib(5))
        self.assertEqual(8, fib(6))
        self.assertEqual(13, fib(7))
        self.assertEqual(21, fib(8))
        self.assertEqual(34, fib(9))
        self.assertEqual(55, fib(10))
        self.assertEqual(89, fib(11))
        self.assertEqual(144, fib(12))

if __name__ == '__main__':
    unittest.main()

# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :

実行結果 (通常実行と -v オプション付き実行)

$ python fibonacci_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

$ python fibonacci_test.py -v
test (__main__.FibonacciTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

次に、引数チェックのテストを導入します。

fibonacci_test2.py (メソッドのみ抜粋)

    def test_invalid_argument(self):
        try:
            fib(1.0)
            self.fail("Did not raise AssertionError")
        except AssertionError:
            pass
        try:
            fib("1000")
        except AssertionError:
            pass
        else:
            self.fail("Did not raise AssertionError")

実行結果

$ python fibonacci_test2.py -v
test (__main__.FibonacciTest) ... ok
test_invalid_argument (__main__.FibonacciTest) ... ok

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

OK

小数点を含む数字や文字列を渡した場合には例外が投げられており、 その場合にはテストがパスするようになっています。

では、負の数を与えた場合はどうでしょうか?

fibonacci_test3.py (テスト関数のみ抜粋)

def test_undefined(self):
    fib(-1)
setattr(FibonacciTest, 'test_引数が日本語の場合は?', test_undefined)

Note

メソッド名に日本語を使えませんが、 setattr() で無理矢理に割り当ててあげることはできます。 どうしてもテストケースを日本語で管理したい場合には便利かもしれません。 (基本的には英語で記述すべきですが)

実行結果

$ python fibonacci_test3.py -v 2>test-error.txt

$ head -n 10 test-error.txt
test (__main__.FibonacciTest) ... ok
test_invalid_argument (__main__.FibonacciTest) ... ok
test_引数が日本語の場合は? (__main__.FibonacciTest) ... ERROR

======================================================================
ERROR: test_引数が日本語の場合は? (__main__.FibonacciTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "src/fibonacci_test3.py", line 42, in test_undefined
    fib(-1)

$ tail -n 7 test-error.txt
    assert type(n) == int
RuntimeError: maximum recursion depth exceeded while calling a Python object

----------------------------------------------------------------------
Ran 3 tests in 0.016s

FAILED (errors=1)

負の数から開始してしまうと再帰の行き着く先がありませんので無限ループになってしまいます。 適切に fibonacci.py を改修してください。 こうしてテストと実装を繰り返すことで境界値条件が明確になり、変更の影響範囲を絞ることが可能になります。

再帰を使ったフィボナッチ数列の実装は、N が 30 くらいから非常に遅くなります。 数学的な解決策としては、三項間漸化式を解いて N に関する方程式を導出する方法があります。 Wikipedia のページなどを参考にして、異なる実装を考えてみるのも良いでしょう。

また、フィボナッチ数列のように繰り返し計算する必要があるものでは 「メモ化」という技法を採用することもあります。 こちらも詳しくは Wikipedia のページなどを参考にしてください。 なお「エキスパート Python」という書籍でもメモ化の実装に触れられています。

doctest の実行方法

doctest はテストケースをスクリプトに直接記述します。 コメントの文字列を解析して、インタープリタへの入力と出力を比較してくれます。 インタープリタで実行した結果をそのままテストとして保存できますので、 比較的簡単に導入できるでしょう。

まずはテスト対象となるスクリプトを記述します。 datetime モジュールを使って日付の文字列を変換してみましょう。

以下のふたつの文字列を datetime オブジェクトに変換し、差分を計算してみます。

  • 2012-01-14 07:56:02
  • 2012-01-14 04:46:30 +0900

datestring_convert.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import datetime


def datestring_convert(s):
    """Convert datetime string which appears in subversion commit log.
    """
    assert type(s) == str, "Argument must be string"
    dt = s.split()
    year, month, day = map(int, dt[0].split("-"))
    hour, minute, second = map(int, dt[1].split(":"))
    return datetime.datetime(year, month, day, hour, minute, second)


if __name__ == '__main__':
    TEST_1 = "2012-01-14 07:56:02"
    TEST_2 = "2012-01-14 04:46:30 +0900"
    d1 = datestring_convert(TEST_1)
    d2 = datestring_convert(TEST_2)
    diff = d1 - d2
    print("{} ==> {}".format(TEST_1, TEST_2))
    print("DIFF: days={}, seconds={}".format(diff.days, diff.seconds))

# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :

実行結果

$ python datestring_convert.py
2012-01-14 07:56:02 ==> 2012-01-14 04:46:30 +0900
DIFF: days=0, seconds=11372

次に、 if __name__ == '__main__': の部分をインタープリタで実行します。 スクリプトを読み込めるように、スクリプトと同じディレクトリに移動してから python を起動します。 (環境変数 PYTHONPATH かモジュール検索パス sys.path を調整しても構いません。)

>>> from datestring_convert import datestring_convert
>>> TEST_1 = "2012-01-14 07:56:02"
>>> TEST_2 = "2012-01-14 04:46:30 +0900"
>>> d1 = datestring_convert(TEST_1)
>>> d2 = datestring_convert(TEST_2)
>>> diff = d1 - d2
>>> print("{} ==> {}".format(TEST_1, TEST_2))
2012-01-14 07:56:02 ==> 2012-01-14 04:46:30 +0900
>>> print("DIFF: days={}, seconds={}".format(diff.days, diff.seconds))
DIFF: days=0, seconds=11372

正常に実行できることを確認できたら、メソッドのコメントに そのまま 貼り付けます。 (>>> プロンプトも含めて「そのまま」です) 使い方としてのドキュメントであり、同時にテストケースにもなる点が doctest のメリットです。 スクリプトを直接実行したときにテストが実行されるように、main ブロックで doctest.testmod() を呼び出しておきましょう。

datestring_convert_with_test.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import datetime


def datestring_convert(s):
    """Convert datetime string which appears in subversion commit log.

    >>> TEST_1 = "2012-01-14 07:56:02"
    >>> TEST_2 = "2012-01-14 04:46:30 +0900"
    >>> d1 = datestring_convert(TEST_1)
    >>> d2 = datestring_convert(TEST_2)
    >>> diff = d1 - d2
    >>> print("{} ==> {}".format(TEST_1, TEST_2))
    2012-01-14 07:56:02 ==> 2012-01-14 04:46:30 +0900
    >>> print("DIFF: days={}, seconds={}".format(diff.days, diff.seconds))
    DIFF: days=0, seconds=11372
    """
    assert type(s) == str, "Argument must be string"
    dt = s.split()
    year, month, day = map(int, dt[0].split("-"))
    hour, minute, second = map(int, dt[1].split(":"))
    return datetime.datetime(year, month, day, hour, minute, second)


if __name__ == '__main__':
    import doctest
    doctest.testmod()

# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :

Python の doctest モジュールにスクリプトを渡してみます。 Python モジュールの実行例としても分かりやすいですね。

$ python -m doctest datestring_convert_with_test.py

何も表示されずに終了しました。 テストが正常に実行され、すべてのテストがパスしたのです。 詳細なログが欲しい場合は -v オプションを付けてください。 実行する処理と期待値が列挙されます。

$ python -m doctest -v datestring_convert_with_test.py
( ... 出力が大量なので省略 ... )

試しにテスト文字列を変更してから実行すると、テストが失敗することを確認できます。

文字列を日付型に変換するためには strptime() も使えます。 テストケースはそのままで、実装を変更してみてください。 テストを記述しておくことによって外部仕様はそのままに、内部仕様を変更していくことが可能になります。

«  プロジェクトの構成   ::   Contents   ::   Nose を使ったテスト  »