Traffine I/O

日本語

2023-02-17

unittest

unittest とは

Pythonのunittestは、Pythonの標準ライブラリに含まれるテストフレームワークです。unittestを使用すると、Pythonコードのユニットテストを書くことができます。

unittestは、次の機能を提供します。

  • テストクラスの作成
  • テストメソッドの作成
  • テストの自動実行
  • アサーションの使用
  • テストのスキップや無視

以下は、unittestを使用してテストを書く例です。

python
import unittest

class MyTest(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

    def test_subtraction(self):
        self.assertEqual(3 - 1, 2)

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

この例では、MyTestという名前のテストクラスを定義し、test_additiontest_subtractionという2つのテストメソッドを定義しています。それぞれ、1 + 1が2に等しいことと、3 - 1が2に等しいことをアサーションしています。

最後に、unittest.main()を呼び出してテストを自動実行しています。

unittestを使用することで、コードの品質を向上させ、バグを見つけやすくなります。また、開発者がコードを修正する際に、テストが失敗したことを知らせてくれるため、開発プロセス全体を改善することができます。

unittest の用語

Pythonのunittestには、次のような用語があります。

用語 説明
テストケース(Test case) unittest で実行されるテストの単位。通常、1 つのテスト関数に対応する。
テストスイート(Test suite) 複数のテストケースをグループ化したもの。unittest.TestSuiteオブジェクトを使用して作成する。
テストランナー(Test runner) テストスイートを実行するためのツール。unittest.TextTestRunnerunittest.XMLTestRunnerなどがある。
テストフィクスチャ(Test fixture) テストケースの実行に必要な事前準備や後処理を行うためのコード。setUp()tearDown()メソッドによって定義される。
アサーション(Assertion) テスト結果を判定するためのコード。assertEqual()assertTrue()などがある。
テストランナー出力(Test runner output) テストランナーが出力する情報。テスト結果や実行時間などが含まれる。
スキップ(Skip) テストをスキップするための機能。@unittest.skipデコレータやskipTest()メソッドを使用する。
アサーションエラー(Assertion error) アサーションが失敗した場合に発生する例外。
テストランのエラー(Test run error) テストスイートの実行中に発生したエラー。通常は、テスト関数のエラーなどが含まれる。
テストランの失敗(Test run failure) テストスイートの実行中に、アサーションが失敗した場合に発生するエラー。

unittest の手順

  1. テスト対象の関数やクラスを定義します。
  2. unittestのTestCaseクラスを継承したクラスを作成します。
  3. テストメソッドを作成します。テストメソッドは、"test_"で始まるメソッド名にすることが推奨されています。
  4. テストメソッド内で、テスト対象の関数やクラスを呼び出します。
  5. assert文を使用して、期待される結果が得られたかどうかを確認します。
  6. テストクラスをunittest.main()で実行します。

以下に、具体例を示します。ここでは、テスト対象の関数として、文字列を反転させるreverse_string関数を定義し、その関数をテストするテストクラスを作成します。

python
def reverse_string(s):
    return s[::-1]

import unittest

class TestReverseString(unittest.TestCase):
    def test_reverse_string(self):
        self.assertEqual(reverse_string('hello'), 'olleh')
        self.assertEqual(reverse_string('world'), 'dlrow')

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

上記のコードでは、テストクラスとしてTestReverseStringクラスを定義し、その中にテストメソッドとしてtest_reverse_stringメソッドを作成しています。test_reverse_stringメソッドでは、reverse_string関数に対して、文字列を渡して返り値が正しいかどうかをassertEqualメソッドを使用して確認しています。

最後に、if name == 'main':の部分で、このファイルが直接実行された場合にunittest.main()を呼び出してテストを実行しています。

unittest の詳細な使い方

unittestの詳細な使い方について説明します。

テストクラスの作成

unittestを使用する場合、テストケースを定義するためにテストクラスを作成する必要があります。テストクラスは、unittest.TestCaseクラスを継承する必要があります。テストメソッドを定義する場合は、メソッド名がtest_で始まる必要があります。

python
import unittest

class MyTest(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

    def test_subtraction(self):
        self.assertEqual(3 - 1, 2)

アサーションの使用

unittestでは、アサーションを使用してテストの成否を判断します。アサーションは、テストの実行結果が期待通りであることを確認するために使用されます。

例えば、次のようなアサーションを使用して、値が等しいことを確認できます。

python
import unittest

class MyTest(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

    def test_subtraction(self):
        self.assertEqual(3 - 1, 2)

テストの自動実行

unittestを使用する場合、テストを自動的に実行することができます。もっとも簡単な方法は、unittest.main()を呼び出すことです。

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

この方法で実行する場合、次のコマンドを実行します。

bash
$ python test_module.py

テストのスキップや無視

テストをスキップする場合は、unittest.skip()を使用します。スキップする理由を指定することもできます。

python
import unittest

class MyTest(unittest.TestCase):
    @unittest.skip("reason for skipping here")
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

unittest のアサーションメソッド

assert文は、テスト対象の関数が期待通りの結果を返すかどうかを検証するために使用されます。例えば、次のようにテストケースを定義することができます。

python
import unittest

def add(a, b):
    return a + b

class MyTest(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)
        self.assertNotEqual(add(2, 3), 6)
        self.assertTrue(add(2, 3) > 4)
        self.assertFalse(add(2, 3) < 4)

上記の例では、add()関数をテスト対象の関数として定義しています。テストケースでは、add()関数が期待通りの結果を返すかどうかを検証しています。例えば、self.assertEqual(add(2, 3), 5)は、add(2, 3)の結果が5であることを検証します。全てのassert文が成功すると、テストケースはパスします。assert文のいずれかが失敗すると、テストケースは失敗します。

以下に、主要なアサーションメソッドを紹介します。

assertEqual(a, b)

aとbが等しいことを検証します。a == bと等価です。

python
self.assertEqual(1 + 2, 3)

assertNotEqual(a, b)

aとbが等しくないことを検証します。a != bと等価です。

python
self.assertNotEqual(1 + 2, 4)

assertTrue(x)

xがTrueであることを検証します。

python
self.assertTrue(1 + 2 == 3)

assertFalse(x)

xがFalseであることを検証します。

python
self.assertFalse(1 + 2 == 4)

assertIs(a, b)

aとbが同じオブジェクトであることを検証します。a is bと等価です。

python
self.assertIs([], [])

assertIsNot(a, b)

aとbが同じオブジェクトではないことを検証します。a is not bと等価です。

python
self.assertIsNot([], [])

assertIsNone(x)

xがNoneであることを検証します。x is Noneと等価です。

python
self.assertIsNone(None)

assertIsNotNone(x)

xがNoneでないことを検証します。x is not Noneと等価です。

python
self.assertIsNotNone(1 + 2)

assertIn(a, b)

aがbに含まれていることを検証します。a in bと等価です。

python
self.assertIn(1, [1, 2, 3])

assertNotIn(a, b)

aがbに含まれていないことを検証します。a not in bと等価です。

python
self.assertNotIn(4, [1, 2, 3])

assertIsInstance(a, b)

aがbのインスタンスかどうかを確認します。

python
self.assertIsInstance([], list)

assertNotIsInstance(a, b)

aがbのインスタンスでないかどうかを確認します。

python
self.assertNotIsInstance([], dict)

assertAlmostEqual(a, b, places=x)

aとbが指定された桁数内で等しいかどうかを確認します。

python
self.assertAlmostEqual(1/3, 0.3333333333333333, places=5)

assertNotAlmostEqual(a, b, places=x)

aとbが指定された桁数内で等しくないかどうかを確認します。

python
self.assertNotAlmostEqual(1/3, 0.333333333, places=7)

assertRaises(exc, func, *args, **kwargs)

指定した例外が発生することを期待して、テストを実行します。つまり、テスト対象の関数が例外を発生させることを確認するために使用されます。

assertRaisesメソッドは、2つの引数を受け取ります。第1引数には、期待される例外の型を指定し、第2引数には、テスト対象の関数を渡します。以下は、assertRaisesメソッドを使用した例です。

python
import unittest

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError('division by zero')
    return a / b

class MyTest(unittest.TestCase):
    def test_divide(self):
        self.assertRaises(ZeroDivisionError, divide, 1, 0)

上記の例では、divide()関数がゼロ除算の場合にZeroDivisionError例外を発生させるように定義されています。MyTestクラスのtest_divideメソッドでは、assertRaisesメソッドを使用して、divide()関数がZeroDivisionError例外を発生させることを確認しています。assertRaisesメソッドの第1引数には、ZeroDivisionErrorを指定し、第2引数には、divide()関数とその引数を渡します。つまり、divide(1, 0)を実行して、ZeroDivisionErrorが発生することを期待しています。

assertRaisesメソッドは、指定した例外が発生しなかった場合にテストを失敗させます。また、例外が発生した場合には、例外オブジェクトが返されます。例外オブジェクトを使用して、例外が正しい種類のものであることを検証することもできます。例えば、次のようにassertRaisesメソッドを使用することができます。

python
import unittest

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError('division by zero')
    return a / b

class MyTest(unittest.TestCase):
    def test_divide(self):
        with self.assertRaises(ZeroDivisionError) as cm:
            divide(1, 0)
        self.assertEqual(str(cm.exception), 'division by zero')

上記の例では、assertRaisesメソッドをwith文で使用しています。with文を使用することで、assertRaisesメソッドの返り値である例外オブジェクトを取得し、例外が正しい種類のものであることを検証しています。cm.exceptionは、例外オブジェクト自体を表します。str(cm.exception)は、例外オブジェクトのエラーメッセージを表す文字列を返します。self.assertEqualメソッドを使用して、cm.exceptionのエラーメッセージが期待されるものであるかを検証しています。

assertRaisesメソッドは、単一の例外のみをチェックできます。複数の例外をチェックする場合は、assertRaisesメソッドを複数回呼び出す必要があります。また、引数を渡さない場合は、AssertionErrorを発生させます。

例えば、次のようにassertRaisesメソッドを使用することができます。

python
import unittest

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError('division by zero')
    elif a < 0:
        raise ValueError('a must be non-negative')
    return a / b

class MyTest(unittest.TestCase):
    def test_divide(self):
        self.assertRaises(ZeroDivisionError, divide, 1, 0)
        self.assertRaises(ValueError, divide, -1, 2)

上記の例では、divide()関数がゼロ除算またはValueError例外を発生させるように定義されています。MyTestクラスのtest_divideメソッドでは、assertRaisesメソッドを使用して、divide()関数が指定した例外を発生させることを確認しています。assertRaisesメソッドを複数回呼び出すことで、複数の例外をチェックしています。

なお、assertRaisesメソッドは、コンテキストマネージャーを使用して次のように書くこともできます。

python
import unittest

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError('division by zero')
    elif a < 0:
        raise ValueError('a must be non-negative')
    return a / b

class MyTest(unittest.TestCase):
    def test_divide(self):
        with self.assertRaises(ZeroDivisionError):
            divide(1, 0)
        with self.assertRaises(ValueError):
            divide(-1, 2)

この場合、assertRaisesメソッドの第1引数に例外を指定する必要はありません。また、コンテキストマネージャーを使用することで、withブロック内で発生した例外のみがキャッチされます。

unittest のテストランナー

unittestでは、テストの結果を報告するためのテストランナーが提供されています。テストランナーは、テストの実行を管理し、結果を報告するためのインターフェースを提供します。以下に、unittestで利用可能な主要なテストランナーを紹介します。

unittest.main()

このテストランナーは、unittestが提供するデフォルトのテストランナーです。このランナーを使うと、コマンドラインからテストを実行することができます。次のように、テストファイルを直接実行することでテストを実行することができます。

bash
$ python test_module.py

unittest.TextTestRunner()

このテストランナーは、テストの結果をテキスト形式で報告するためのものです。次のように、テスト結果をコンソールに表示することができます。

python
import unittest

if __name__ == '__main__':
    suite = unittest.TestLoader().discover('.')
    unittest.TextTestRunner().run(suite)

unittest.HTMLTestRunner()

このテストランナーは、テストの結果をHTML形式で報告するためのものです。次のように、HTML形式のレポートを生成することができます。

python
import unittest
import HTMLTestRunner

if __name__ == '__main__':
    suite = unittest.TestLoader().discover('.')
    with open('test_report.html', 'wb') as f:
        HTMLTestRunner.HTMLTestRunner(stream=f).run(suite)

unittest の特別なメソッド

unittestには、テストのセットアップやクリーンアップを行うための特別なメソッドがあります。それらのメソッドを使うことで、テストコードをより効果的に書くことができます。以下に、主要な特別なメソッドを紹介します。

setUp()

setUp()メソッドは、テストケースの実行前に実行される処理を定義します。例えば、テストケースで共通して使用するオブジェクトの初期化や、データベースの接続を確立するなどの処理を定義することができます。setUp()メソッドは、テストケースごとに一度だけ実行されます。

python
import unittest

class MyTest(unittest.TestCase):
    def setUp(self):
        self.my_object = MyObject()

    def test_something(self):
        result = self.my_object.do_something()
        self.assertEqual(result, expected_result)

上記の例では、setUp()メソッドでMyObjectオブジェクトを初期化し、テストケースで使用できるようにしています。

tearDown()

tearDown()メソッドは、テストケースの実行後に実行される処理を定義します。例えば、データベースの接続を切断するなどの処理を定義することができます。tearDown()メソッドは、テストケースごとに一度だけ実行されます。

python
import unittest

class MyTest(unittest.TestCase):
    def setUp(self):
        self.my_object = MyObject()

    def tearDown(self):
        self.my_object.close()

    def test_something(self):
        result = self.my_object.do_something()
        self.assertEqual(result, expected_result)

上記の例では、tearDown()メソッドでMyObjectオブジェクトの接続を切断しています。

setUp() と tearDown() の継承

setUp()tearDown()メソッドは、継承を利用して複数のテストケースで共通の前処理・後処理を定義することもできます。例えば、複数のテストケースで共通の初期化処理を行う場合は、共通の親クラスを作成し、setUp()メソッドを親クラスに定義します。

python
import unittest

class MyObjectTestCase(unittest.TestCase):
    def setUp(self):
        self.my_object = MyObject()

    def tearDown(self):
        self.my_object.close()

class TestSomething(MyObjectTestCase):
    def test_something(self):
        result = self.my_object.do_something()
        self.assertEqual(result, expected_result)

class TestSomethingElse(MyObjectTestCase):
    def test_something_else(self):
        result = self.my_object.do_something_else()
        self.assertEqual(result, expected_result)

上記の例では、MyObjectTestCaseクラスが共通の初期化処理を行う親クラスとして定義されています。TestSomethingクラスとTestSomethingElseクラスは、それぞれMyObjectTestCaseクラスを継承しており、MyObjectオブジェクトを使用するためのsetUp()メソッドとtearDown()メソッドが自動的に継承されます。

このように、unittestではsetUp()tearDown()メソッドを利用して、テストケースの前処理・後処理を柔軟に定義することができます。また、クラスや継承を利用することで、複数のテストケースで共通の前処理・後処理を簡単に実装することができます。

setUpClass() と tearDownClass()

setUpClass()は、テストクラスが実行される前に呼び出されるメソッドです。このメソッドは、テストクラスで使用されるリソースをセットアップするために使用されます。

tearDownClass()は、テストクラスが実行された後に呼び出されるメソッドです。このメソッドは、全てのテストが完了した後に実行され、テストクラスで使用されたリソースをクリーンアップするために使用されます。

setUpClass()メソッドやtearDownClass()メソッドは、次のように定義されます。

python
@classmethod
def setUpClass(cls):
    pass

@classmethod
def tearDownClass(cls):
    pass

setUpClass()メソッドやtearDownClass()メソッドは、クラスメソッドとして定義されます。このため、このメソッドはクラスレベルで実行され、クラス変数やクラスメソッドを使用することができます。

setUpClass()メソッドは、例えば、データベースコネクションを確立したり、テスト用のファイルを作成したり、他のリソースをセットアップするために使用することができます。

tearDownClass()メソッドは、例えば、データベースコネクションをクローズしたり、テスト用のファイルを削除したり、他のリソースを解放するために使用することができます。

以下は、setUpClass()メソッドやtearDownClass()メソッドを使用する例です。

python
import unittest
import os

class MyTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.test_dir = 'test'
        os.makedirs(cls.test_dir)

    def test_example(self):
        test_file = os.path.join(self.test_dir, 'test.txt')
        with open(test_file, 'w') as f:
            f.write('test')

    @classmethod
    def tearDownClass(cls):
        os.rmdir(cls.test_dir)

上記の例では、setUpClass()メソッドでtestという名前のディレクトリを作成しています。test_example()メソッドでは、test.txtというファイルをtestディレクトリに作成しています。tearDownClass()メソッドでtestディレクトリを削除しています。このように、テストで使用するリソースをsetUpClass()メソッドでセットアップすることで、テスト間の相互影響を避けることができます。

subTest

subTest()は、Pythonのunittestで複数の入力値を使用して同じテストを繰り返し実行するために使用される機能です。通常、単一のテスト関数に対して、複数の異なる入力値を使用してテストを行う場合に使用されます。

subTest()を使用すると、テストケースが失敗した場合に、失敗した入力値を特定することができます。また、subTest()を使用することで、テスト関数が失敗しても、それ以降のテストケースの実行を続けることができます。

以下は、subTest()を使用した例です。

python
import unittest

class TestMath(unittest.TestCase):

    def test_add(self):
        test_cases = [(2, 3, 5), (-2, 3, 1), (0, 0, 0)]
        for a, b, expected in test_cases:
            with self.subTest(a=a, b=b):
                result = a + b
                self.assertEqual(result, expected)

この例では、test_add()関数が、複数の異なる入力値を使用して同じテストを実行します。subTest()を使用することで、各入力値のテスト結果を独立して表示することができます。

subTest()は、withステートメントと共に使用されます。withブロック内でアサーションを行うことで、入力値ごとにテストケースが独立して実行されます。また、subTest()の引数には、キーワード引数として渡される入力値を指定することができます。

unittest を使ってテストを書くときの注意点

以下にunittestを使ってテストを書くときの注意点を紹介します。

  • テストデータの適切な準備
    テストを書く際には、適切なテストデータを用意する必要があります。データが不足している場合や、間違ったデータを使用している場合は、テストが不十分になる可能性があります。また、テストが複雑になることもあります。
  • テストの依存関係を避ける
    テストの実行順序に依存するようなテストを書くのは避けるべきです。それらは脆弱で、テストの実行順序が変わると失敗することがあります。
  • テストの粒度
    テストの粒度は、非常に重要です。小さなテストを書くことで、問題が発生した場合にその原因を特定しやすくなります。また、小さなテストを組み合わせて大きなテストを作成することもできます。
  • エラーメッセージの分かりやすさ
    テストが失敗した場合、エラーメッセージが分かりにくいと問題の特定に時間がかかることがあります。エラーメッセージは明確かつ分かりやすくするように努めるべきです。
  • テストの実行順序
    unittestでは、テストの実行順序が保証されていないため、テスト間で状態が共有されることがあります。そのため、各テストメソッドは、前のテストメソッドの影響を受けずに独立して実行されるようにする必要があります。

参考

https://docs.python.org/2.7/library/unittest.html
https://docs.python.org/3/library/unittest.mock-examples.html

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!