Python 3のunittestを使い始めるためのメモ


このエントリーをはてなブックマークに追加

Pythonunittestをちゃんと使ったことがなかったので、使い方について調べてまとめました。Windows 10のAnaconda Python 3.8.5で動作確認しています。

もっとも小さな例

被テストコードとテストコードとを同一のスクリプトに記述した小さな例を以下に示します。実際はこんなことはほぼないと思いますが。

import unittest


def add(x, y):
    return x + y


class AddTest(unittest.TestCase):

    def test_add(self):
        self.assertEqual(add(1, 2), 3)


def main():
    unittest.main()


if __name__ == '__main__':
    main()

ここで、被テストコードが関数add()、テストコードがAddTestです。

テストコードは以下の作法にのっとって作成される必要があります。

  • unittest.TestCaseクラスを継承したサブクラスを作成(上記のAddTestクラス)
  • サブクラスの中に、testで始まるメソッドを作成
    • メソッド名がtestで始まらないと実行されないので注意
  • testで始まるメソッドの中で、self.assertEqual()self.assertTrue()などを呼び出してテストを記述

このスクリプトmain.pyとして保存しpython main.pyと実行すると、テストが無事走ります。

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Pythonパッケージに対するテスト

もう少しリアルな例として、あるPythonパッケージに対してテストを行う例を考えます。

ディレクトリ構造

以下のディレクトリ構造でコードが配置されているものとします。

root_dir/
    ├─comp_prog_lib/
    │  │  __init__.py
    │  │
    │  └─math/
    │          prime.py
    │          __init__.py
    │
    └─tests/
        │  __init__.py
        │
        └─math_tests/
                test_prime.py
                __init__.py

root_dirは、例えばGitのリポジトリに相当するような上位ディレクトリであるものとします。

comp_prog_libは、被テスト対象であるPythonパッケージです。ここでは競技プログラミングのためのライブラリという意味でcomp_prog_libと名付けています。comp_prog_libの下には、競技プログラミングのためのモジュールがある階層構造とともに配置されているものとします。

testsは、comp_prog_libをテストするためのコードです。テストコードは普通ライブラリとは分離して配置したくなるはずなので、comp_prog_libと並べて配置します。

prime.pyのコードを以下に示します。整数n素数かどうかを判定する関数が実装されています。

"""
素数に関する関数を提供
"""


def is_prime(n: int):
    """nが素数か否かを判定"""
    if n <= 1:
        return False
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1
    return True

test_prime.pyのコードを以下に示します。整数1から16までの素数判定が正しく行えているかをチェックするテストが実装されています。

"""
prime.pyのテストスクリプト
"""
import unittest

from comp_prog_lib.math.prime import is_prime


class IsPrimeTest(unittest.TestCase):

    def test_is_prime(self):
        primes = set([2, 3, 5, 7, 11, 13])
        for i in range(1, 17):
            if i in primes:
                self.assertTrue(is_prime(i))
            else:
                self.assertFalse(is_prime(i))

__init__.pyはすべてパッケージを構成するための特殊ファイルで、中身は空です。

Test Discoveryによるテスト実行

テストを実行するには、root_dir/cdした状態で、

$ python -m unittest discover tests

と実行します。すると、testsディレクトリ以下に存在する、ファイル名がtestで始まるテストスクリプトが自動で収集され実行されます。これはTest Discoveryと呼ばれる仕組みです。実行結果は以下です。

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

なお、tests/以下のディレクトリ構造には注意が必要そうです。最初、上記でmath_tests/と示していたディレクトリ名をmath/にしていたのですが、この状態でテスト実行コマンドを実行すると、

E
======================================================================
ERROR: math.test_prime (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: math.test_prime
Traceback (most recent call last):
  File "(環境名)\lib\unittest\loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "(環境名)\lib\unittest\loader.py", line 377, in _get_module_from_name
    __import__(name)
ModuleNotFoundError: No module named 'math.test_prime'; 'math' is not a package


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

というかなりわかりにくいエラーが出て失敗しました。tests/mathcomp_prog_lib/mathと名前衝突していたのがよくなかったようで、tests/mathtests/math_testsと名前を変えることでエラーが消えました(参考)。正直、理屈はよくわかっていません。

ディレクトリ構造についての補足

comp_prog_lib/パッケージのテストがうまくいった理由について説明します。Pythonは、カレントディレクトリが暗黙的にsys.pathに加えられる仕様を持ちます。なのでroot_dir/cdした状態では、comp_prog_libがimport可能な状態になります。なのでtest_prime.pyfrom comp_prog_lib.math.prime import is_primeというimport文が正しく動きます。

これは場合によっては害になることもありえます。例えば、comp_prog_libに相当する部分をpip installなどでインストール可能なように設定していた場合、ローカルのcomp_prog_libをテストしているのかインストール済のcomp_prog_libをテストしているのかが不明瞭になります。この問題を避けるため、Pytestのドキュメント では、comp_prog_libsrcディレクトリの下に移動させるベストプラクティスを推奨しています。これはunittestでも通じる話だと思います。

参考URL