HOWTO · Python

Python モック レイズ例外

このチュートリアルでは、モック モジュールを使用して単体テストを実行するときに例外を発生させる方法を示します。

このページの内容

この記事の主な目的は、単体テスト ライブラリ unittest を使用するときに例外をスローする方法を示すことです。

Python で単体テスト ライブラリ unittest を使用するときに例外をスローする

コードが安定しており、プロジェクトのスコープ内のほとんどのユース ケースに対応していることを確認するには、さまざまな入力を与えてテストする必要があります。 Python に存在するライブラリの 1つである unittest は、そのような機能を提供し、さまざまなテスト ケースを記述して実行できるようにします。

テスト中は、入力が予期しない場合を処理できる必要があります。 このようなケースは、指定された入力の何が問題なのかを詳しく説明する例外をスローすることで処理できます。

次のコードを検討してください。

class A:
    def __init__(self):
        pass

    def computeData(self):
        return 0


class B:
    def method_to_test():
        obj = A()
        try:
            print(obj.computeData())
        except Exception as e:
            print("Exception at method_to_test: " + str(e))

テストしたい関数が method_to_test だとしましょう。 別のクラス A のオブジェクトを初期化し、computeData という名前のメソッドの 1つを try-except ブロックで呼び出して例外をキャッチします。

特定の例外がスローされたときの computeData の動作をテストしたい場合は、unittest モジュールを使用して行う必要があります。

例外を 1 回発生させる

上記のシナリオを考えると、いつ例外をスローするかをより細かく制御したい場合もあります。

そのような必要性が生じる理由はたくさんあります。 コードの実装によっては、動作をより詳細に制御できると非常に役立ちます。

例外をアサート

テスト ケースを作成しているときに、例外がトリガーされるかどうかを確認したい場合があります。 これは、例外がスローされたかどうかを知りたい場合に特に便利です。

これは、何か問題が発生した場合に、テスト ケースの早い段階で見つけるのに役立ちます。

サードパーティ モジュールのモック

サードパーティのモジュールは、実装に応じて、ほとんどない場合から多数の場合までさまざまです。 サードパーティ製モジュールが意図したとおりに動作するかどうかをテストするには、さまざまな出力でその動作もテストする必要があります。

そのような例の 1つは request です。これは、ネットワーク経由で通信したり、HTTP を使用してデータを送受信したりするときに広く使用されているモジュールです。

関数から手動で例外を発生させる

computeData 関数から手動で例外をスローするには、side_effect 属性を使用する必要があります。

次のコードを検討してください。

import unittest
from unittest.mock import patch
from unittest.mock import MagicMock


class A:
    def __init__(self) -> None:
        pass

    def computeData(self):
        return 0


class B:
    def method_to_test():
        obj = A()
        try:
            print(obj.computeData())
        except Exception as e:
            print("Exception at method_to_test: " + str(e))


class Test(unittest.TestCase):
    @patch.object(A, "computeData", MagicMock(side_effect=Exception("Test")))
    def test_method(self):
        B.method_to_test()


if __name__ == "__main__":
    Test().test_method()

出力:

Exception at method_to_test: Test

このソリューションでは、メソッド method_to_test をテストするために、新しいメソッド test_method が作成されます。 関数が patch.object で装飾されていることに注意することも重要です。

patch デコレーターは、モジュールとクラスレベルの属性にパッチを適用するためにモジュールで提供されます。 何にパッチを当てるかをより具体的にするために、patch の代わりに patch.object を使用してメソッドに直接パッチを当てます。

デコレーターでは、クラス名 A が渡されます。 これは、パッチが適用されるオブジェクトが computeData というメンバー名が渡された A の一部であることを示します。

最後のパラメーターは、Mock のサブクラスである MagicMock オブジェクトです。この場合、side_effect 属性を使用して、必要な動作を computeData に関連付けます。この場合、例外をスローします。

プログラムの大まかな流れは次のとおりです。

  1. test_method が呼び出されます。
  2. クラス A の computeData にパッチが適用されます。 side_effect が割り当てられていますが、この場合は例外です。
  3. クラス B の method_to_test が呼び出されます。
  4. クラス A のコンストラクターが呼び出され、インスタンスが obj に格納されます。
  5. computeData が呼び出されます。 メソッドにパッチが適用されているため、例外がスローされます。

例外を 1 回発生させる

例外を 1 回だけ発生させるか、関数の動作をより詳細に制御するために、side_effect は単なる関数呼び出し以上のものをサポートします。

現在、side_effect は以下をサポートしています:

  1. 反復可能
  2. 呼び出し可能
  3. 例外 (インスタンスまたはクラス)

Iterable を使用すると、メソッドで例外を 1 回だけ発生させることができます。

class Test(unittest.TestCase):
    @patch.object(A, "computeData", MagicMock(side_effect=[1, Exception("Test"), 3]))
    def test_method(self):
        B.method_to_test()


if __name__ == "__main__":
    testClass = Test()
    testClass.test_method()
    testClass.test_method()
    testClass.test_method()

出力:

1
Exception at method_to_test: Test
3

渡された Iterable により、選択した要素にパッチが適用され、いつ例外を発生させるかを制御できるようになります。 したがって、関数が 2 回目に呼び出されたときにのみ例外がスローされ、それ以外の場合は 1 と 3 が返されます。

例外をアサート

特定の例外が発生したかどうかを判断するには、目的の例外がスローされない場合に unittest.TestCase.assertRaises を使用できます。

class Test(unittest.TestCase):
    @patch.object(A, "computeData", MagicMock(side_effect=Exception("Test")))
    def test_method(self):
        self.assertRaises(KeyError, A.computeData)


if __name__ == "__main__":
    Test().test_method()

出力:

Exception at method_to_test: Test
Traceback (most recent call last):
  File "d:\Python Articles\a.py", line 28, in <module>
    Test().test_method()
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1369, in patched
    return func(*newargs, **newkeywargs)
  File "d:\Python Articles\a.py", line 25, in test_method
    self.assertRaises(KeyError, A.computeData)
  File "C:\Program Files\Python310\lib\unittest\case.py", line 738, in assertRaises
    return context.handle('assertRaises', args, kwargs)
  File "C:\Program Files\Python310\lib\unittest\case.py", line 201, in handle
    callable_obj(*args, **kwargs)
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1104, in __call__
    return self._mock_call(*args, **kwargs)
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1108, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1163, in _execute_mock_call
    raise effect
  File "d:\Python Articles\a.py", line 17, in method_to_test
Exception: Test

test_method メソッドに小さな変更を加えて、2つのパラメーターを指定して assertRaises を呼び出すと、上記の出力が得られます。

2つのパラメーターは次のように定義できます。

  1. KeyError - トリガーしたいすべての例外がここに渡されます。
  2. A.computeData - 例外をスローするメソッド。

サードパーティ モジュールのモック

前のように post をモックするには、patch デコレーターを使用する必要があります。 簡単な例は次のとおりです。

class Test(unittest.TestCase):
    @patch(
        "requests.post", MagicMock(side_effect=requests.exceptions.ConnectionError())
    )
    def test_method(self):
        requests.post()

出力:

Traceback (most recent call last):
  File "d:\Python Articles\a.py", line 33, in <module>
    Test().test_method()
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1369, in patched
    return func(*newargs, **newkeywargs)
  File "d:\Python Articles\a.py", line 30, in test_method
    requests.post()
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1104, in __call__
    return self._mock_call(*args, **kwargs)
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1108, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
  File "C:\Program Files\Python310\lib\unittest\mock.py", line 1163, in _execute_mock_call
    raise effect
requests.exceptions.ConnectionError

patch デコレータを使用して、サードパーティ モジュールの関数にパッチを適用することもできます。これにより、デバッグが改善され、製品/プログラムの信頼性の規模をより詳細に制御できるようになります。