Pythonのunittestを使ってみたメモ

Pythonのunittestを使ってみたメモ。またそのうち更新するかも。

# テストの作成と実行

まずは「[unittest --- ユニットテストフレームワーク](https://docs.python.org/ja/3/library/unittest.html)」を使うところから。次のようなテストモジュールを書く。

* `import unittest` する
* `unittest.TestCase` を継承した試験クラスを書く
* 試験クラスには `test_` で始まる名前のテストメソッドを書く
* スクリプト単体で呼出された（`__name__ == "__main__"`）時は`unittest.main()`

unittestの「[基本的な例](https://docs.python.org/ja/3/library/unittest.html#basic-example)」から。

~~~
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

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

# 利用できるassertメソッド

「[アサートメソッド](https://docs.python.org/ja/3/library/unittest.html#assert-methods)」を参照。

まず各種演算子に対応したもの。

* [assertEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertEqual)
* [assertNotEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertNotEqual)
* [assertTrue(x)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertTrue)
* [assertFalse(x)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertFalse)
* [assertIs(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertIs)
* [assertIsNot(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertIsNot)
* [assertIsNone(x)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertIsNone)
* [assertIsNotNone(x)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertIsNotNone)
* [assertIn(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertIn)
* [assertNotIn(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertNotIn)
* [assertGreater(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertGreater)
* [assertGreaterEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertGreaterEqual)
* [assertLess(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertLess)
* [assertLessEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertLessEqual)

インスタンスがクラスを継承している/いないか確認するもの。[isinstance()](https://docs.python.org/ja/3/library/functions.html#isinstance)の結果が確認される。文字列型かint型とかもそれぞれのクラスを継承しているので `assertIsInstance("test string", str)` のようにして検証できた。

* [assertIsInstance(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertIsInstance)
* [assertNotIsInstance(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertNotIsInstance)

関数を実行してエラー、警告、ログ出力が発生することを確認するもの。異常系の動作確認に使えそう。

* [assertRaises(exc, fun, *args, **kwds)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertRaises)
* [assertRaisesRegex(exc, r, fun, *args, **kwds)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertRaisesRegex)
* [assertWarns(warn, fun, *args, **kwds)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertWarns)
* [assertWarnsRegex(warn, r, fun, *args, **kwds)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertWarnsRegex)
* [assertLogs(logger, level)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertLogs)

小数点以下何桁で丸めたときに一致するか、正規表現にマッチするかなどちょっと高度な比較をするもの。

* [assertAlmostEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertAlmostEqual)
* [assertNotAlmostEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertNotAlmostEqual)
* [assertRegex(s, r)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertRegex)
* [assertNotRegex(s, r)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertNotRegex)
* [assertCountEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertCountEqual)

以下は内部メソッドと思っておくとよいもの。assertEqual()が内部で使用するメソッド。

* [assertMultiLineEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertMultiLineEqual)
* [assertSequenceEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertSequenceEqual)
* [assertListEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertListEqual)
* [assertTupleEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertTupleEqual)
* [assertSetEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertSetEqual)
* [assertDictEqual(a, b)](https://docs.python.org/ja/3/library/unittest.html#unittest.TestCase.assertDictEqual)

# テストの一括実行（ディスカバリー）

テストモジュールのあるディレクトリで以下を実行する。

~~~
$ python -m unittest discover
~~~

「[テストディスカバリ - unittest --- ユニットテストフレームワーク](https://docs.python.org/ja/3/library/unittest.html#test-discovery)」参照。後でもう少し書く（ことにしたい）。

# テストメソッド名と実行順序

主に「[テストコードの構成 - unittest --- ユニットテストフレームワーク](https://docs.python.org/ja/3/library/unittest.html#organizing-test-code)」参照。ポイントを3点。

* 名前が `test_` で始まるテストメソッドが、名前順で実行される。
* `setUp()` メソッドがある場合、各テストメソッドの実行前に、毎回実行される。
* `tearDown()` メソッドがある場合、各テストメソッドの実行後に、毎回実行される。

言い換えれば、次のように実行される。

* `setUp()` メソッド → 1つ目のテストメソッド → `tearDown()` メソッド
* `setUp()` メソッド → 2つ目のテストメソッド → `tearDown()` メソッド
* `setUp()` メソッド → （以下略）

`setUp()` は試験準備、 `tearDown()` は試験の後片付けに使われるとのこと。 `setUp()` メソッドを実装することで、どのテストメソッドも同じ状態から開始されるようになる。

> setUp() メソッドを実装することで設定コードをくくり出すことができます。[...]同様に、テストメソッド実行後に片付けをする tearDown() メソッドを提供出来ます

`setUp()` メソッドのが失敗した場合、テストメソッドおよび `tearDown()` メソッドは実行されない。テストメソッドは失敗したとしても、 `tearDown()` メソッドは実行される。

> テスト中に setUp() メソッドで例外が発生した場合、フレームワークはそのテストに問題があるとみなし、そのテストメソッドは実行されません。[...]setUp() が成功した場合、テストメソッドが成功したかどうかに関わらず tearDown() が実行されます。

テストメソッドは名前順で実行される。

> いろいろなテストが実行される順序は、文字列の組み込みの順序でテストメソッド名をソートすることで決まります。

意識してテストメソッドの命名を行うと、正常系の試験の後に以上系の試験を実施、などの整理ができてよさそう。 `test_番号_試験内容` のように命名するとして、正常系試験は `test_001_試験内容` から、以上系試験は `test_901_試験内容` からとするとか。

# 一時ファイルを使用したテスト

ファイル読込みのあるモジュールの試験などで、テストファイルを一時ファイルとして用意したかった。次のようにして実現できた。

* `setUp()`メソッドで一時ファイルを作成し、ファイル名をインスタンス変数に格納。
* ファイル作成は[tempfileモジュール](https://docs.python.org/ja/3/library/tempfile.html)を使う。 `tempfile.NamedTemporaryFile` を使用し `delete=False` を指定すると一時ファイルが自動削除されずに残り、以降の試験メソッドで利用できる。
* テストメソッドではインスタンス変数からファイル名を取得し、ファイルアクセス系の試験を行う。
* `tearDown()` メソッドで一時ファイルを削除する。

~~~
import os
import tempfile
import unittest

class TestWithTempFile(unittest.TestCase):
    def setUp(self):
        self.testfile = None
        with tempfile.NamedTemporaryFile(mode='w', delete=False) as testfile:
            self.testfile = testfile.name
            # testfile.write(...)などでテストファイルの内容を作成する
            ...

    def test_xxxx(self):
        # setUpで作成されたファイルのパスがself.testfileに格納されている
        # ファイルを使用した試験を実装する
        self.assertTrue(self.testfile)
        with open(self.testfile, "r") as f:
            ...

    def test_xxxx(self):
        # ファイルがない時の試験をする際は、ファイルを削除してもよい
        os.remove(self.testfile)
        with self.assertRaises(FileNotFoundError):
            # ファイルを開く処理を含む作業など
            ...

    def tearDown(self):
        if os.path.exists(self.testfile):
            os.remove(self.testfile)

if __name__ == '__main__':
    unittest.main(verbosity=2)
~~~

`setUp()` 内でエラーが起きたとき（起きちゃいけないけど）は、`tearDown()` が実行されず一時ファイルがそのまま残っちゃいそうなので、エラー発生時は確認する。

----
本ページ内容は筆者が参照の便のためにある時点でまとめた個人的なメモです。内容を保証するものではなく、また筆者の所属組織等とは一切かかわりがありません。