HOWTO · Python

Python モック クラス属性

このチュートリアルでは、モック モジュールを使用してクラスの属性/メンバーをモックする方法を示します。

このページの内容

この記事の主な目的は、テストとデバッグの目的で、python 単体テスト モジュール unittest を使用してクラス属性を操作する方法を示すことです。

クラス属性をモックする理由

開発したコードのバグ、エラー、およびコーナー ケースをテストすることは、アプリケーションを開発する際の最も重要な側面の 1つです。主にアプリケーションが複数のユーザーを対象としている場合です。

組み込みの Python モジュール unittest を使用して、テスト ケースを実行し、コードの整合性をテストできます。 厳密なテストを必要とする最も一般的な要素の 1つは、クラス属性です。

クラス属性は、予期しない動作を防ぐためにランダムな入力を処理できます。

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

class Calculate:
    value = 22  # Needs to be tested for different data types

    def Process(self):  # An example method
        pass

value という属性と Process という名前のメソッドを含む Calculate という名前のクラスを考えてみましょう。 ディクショナリは value 内に保存され、後で要件とデータ型に基づいて処理されます。

属性がほぼすべてのタイプのディクショナリを格納でき、エラーなしで処理されることを確認するには、属性をテストして、実装にエラーがなく、改訂が必要ないことを確認する必要があります。

クラス属性をモックするための可能な解決策

クラス属性は 2つの方法でモックできます。 PropertyMock を使用し、PropertyMock を使用しない。 サンプルコードを使用して、以下のそれぞれについて学びましょう。

PropertyMock を使用してクラス属性をモックする

属性をモックするには、PropertyMock を使用できます。これは主に、プロパティのモックまたはクラスの記述子として使用することを目的としています。

PropertyMock__get__ および __set__ メソッドを提供して、フェッチされたプロパティの戻り値を変更することに注意してください。

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

import unittest
from unittest.mock import patch
from unittest.mock import MagicMock, PropertyMock


class Calculate:
    value = 22  # Needs to be tested for different data types

    def Process(self):  # An example method
        pass


class Test(unittest.TestCase):
    @patch.object(Calculate, "value", new_callable=PropertyMock)
    def test_method(self, mocked_attrib):
        mocked_attrib.return_value = 1
        # Will pass without any complaints
        self.assertEqual(Calculate().value, 1)
        print("First Assertion Passed!")

        self.assertEqual(Calculate().value, 22)  # Will throw an assertion
        print("Second Assertion Passed!")


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

出力:

First Assertion Passed!
Traceback (most recent call last):
  File "d:\Python Articles\a.py", line 24, 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 20, in test_method
    self.assertEqual(Calculate().value, 22) # Will throw an assertion 
  File "C:\Program Files\Python310\lib\unittest\case.py", line 845, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Program Files\Python310\lib\unittest\case.py", line 838, in _baseAssertEqual
    raise self.failureException(msg)
AssertionError: 1 != 22

このソリューションでは、新しいメソッド test_method が作成され、Calculate.value の値が変更されます。 関数が patch.object で装飾されていることに注意することが重要です。

モジュールの patch デコレーターは、モジュールとクラスレベルの属性にパッチを当てるのに役立ちます。 何にパッチを当てるかをもう少し具体的にするために、patch の代わりに patch.object を使用してメソッドに直接パッチを当てます。

デコレーターでは、最初にクラス名 Calculate が渡されます。これは、パッチが適用されるオブジェクトが Calculate の一部であり、属性 value の名前が渡されることを示します。

最後のパラメーターは PropertyMock オブジェクトで、別の数値を渡すことで value 属性を上書きします。

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

  1. test_method が呼び出されます。
  2. クラス Calculatevalue がパッチされ、new_callable が割り当てられ、PropertyMock のインスタンスが割り当てられます。
  3. Calculate.valuereturn_value プロパティを使用して 1 で上書きされます。
  4. 最初のアサーションがチェックされます。ここで、Calculate.value1 に等しくなければなりません。
  5. 2 番目のアサーションがチェックされます。ここで、Calculate.value22 に等しくなければなりません。

PropertyMock を使用せずにクラス属性をモックする

PropertyMock を使用せずに解決することもできます。 上記の例をわずかに変更するだけで済みます。

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

import unittest
from unittest.mock import patch


class Calculate:
    value = 22  # Needs to be tested for different data types

    def Process(self):  # An example method
        pass


class Test(unittest.TestCase):
    @patch.object(Calculate, "value", 1)
    def test_method(self):
        # Will pass without any complaints
        self.assertEqual(Calculate().value, 1)
        print("First Assertion Passed!")

        # Will throw an assertion because "Calculate.value" is now 1
        self.assertEqual(Calculate().value, 22)
        print("Second Assertion Passed!")


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

出力:

First Assertion Passed!
Traceback (most recent call last):
  File "d:\Python Articles\a.py", line 23, 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 19, in test_method
    self.assertEqual(Calculate().value, 22) # Will throw an assertion because "Calculate.value" is now 1
  File "C:\Program Files\Python310\lib\unittest\case.py", line 845, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Program Files\Python310\lib\unittest\case.py", line 838, in _baseAssertEqual
    raise self.failureException(msg)
AssertionError: 1 != 22

PropertyMock のインスタンスを new_callable に渡す代わりに、保存したい値を直接 Calculate.value に渡すことができます。

Python モック クラス コンストラクター

初期化されたすべての変数が意図したとおりに機能し、意図しない動作を示さないことを確認してください。 コーナーケースを減らすために、さまざまな入力でコンストラクターをテストすることも必要です。

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

class Calculate:
    num = 0
    dic = {}

    def __init__(self, number, dictt):
        self.num = number
        self.dic = dictt

    def Process(self):  # An example method
        pass

クラスCalculateのコンストラクタをテストしたいとしましょう。 属性が意図したとおりに機能することを確認するには、コンストラクターに パッチ を適用し、さまざまな入力を渡して、考えられるエラーを根絶する必要があります。

どうすればそれができますか? 次の解決策を参照してください。

patch.object デコレーターを使用してコンストラクターにパッチを適用する

patch.object デコレーターを使用して、コンストラクターにパッチを適用できます。

import unittest
from unittest.mock import patch
from unittest.mock import MagicMock, PropertyMock


class Calculate:
    num = 0
    dic = {}

    def __init__(self):
        self.num = 1
        self.dic = {"11", 2}

    def Process(self):  # An example method
        pass


class Test(unittest.TestCase):
    @patch.object(Calculate, "__new__")
    def test_method(self, mocked_calc):
        mocked_instance = MagicMock()
        mocked_instance.num = 10
        mocked_instance.dic = {"22": 3}
        mocked_calc.return_value = mocked_instance

        self.assertEqual(Calculate().num, 10)
        self.assertEqual(Calculate().dic, {"22": 3})

        print("First set of Asserts done!")

        self.assertEqual(Calculate().num, 1)
        self.assertEqual(Calculate().dic, {"11", 2})
        print("Second set of Asserts done!")


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

出力:

The first set of Asserts is done!
Traceback (most recent call last):
  File "d:\Python Articles\a.py", line 37, 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 32, in test_method
    self.assertEqual(Calculate().num, 1)
  File "C:\Program Files\Python310\lib\unittest\case.py", line 845, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\Program Files\Python310\lib\unittest\case.py", line 838, in _baseAssertEqual
    raise self.failureException(msg)
AssertionError: 10 != 1

__init__ にパッチを当てる代わりに、__new__ にパッチを当てたことは注目に値します。

__new__ が実行されるとクラスのインスタンスが作成されるのに対し、__init__ では変数のみが初期化されるためです。 では、モック化された新しいインスタンスを作成する必要があるのに、なぜ __init__ の代わりに __new__ にパッチを当てるのでしょうか?