HOWTO · Python

Python で別の関数によって呼び出される 1つの関数にモック パッチを適用する

このチュートリアルでは、モック オブジェクトとその重要性について説明し、 patch() をデコレーター、コンテキスト マネージャーとして使用し、手動で開始/停止して、別の関数によって呼び出される 1つの関数にパッチを適用する方法を示します。

このページの内容

堅牢なコードを書くときはいつでも単体テストが不可欠です。 それらは、アプリケーション ロジックを検証し、コード要件の他のすべての側面を検査するのに役立ちます。 ただし、複雑さや外部依存などのハードルにより、品質の高いテスト ケースを作成するのは困難です。

この時点で、Python モック ライブラリである unittest.mock は、これらの障害を克服するのに役立ちます。 今日は、モック オブジェクトとその重要性、およびターゲットを一時的にモック オブジェクトに置き換える Python patch() を使用したさまざまなサンプル コードについて学習します。

モック オブジェクトとその重要性

モック オブジェクトは、制御された方法で実際のオブジェクトの動作を模倣します。たとえば、テスト環境内でさまざまなテストを実行して、コードが期待どおりに機能することを確認します。 モック オブジェクトを使用するプロセスはモッキングと呼ばれます。 これは、テストの品質を向上させるための強力なツールです。

要点は、なぜあざける必要があるのかということです。 それを使用する根本的な理由は何ですか?

さまざまな原因が、Python でモッキングを使用することの重要性を示しています。 以下でそれらを見てみましょう。

  1. 主な理由の 1つは、モック オブジェクトを使用してコードの動作を改善することです。 たとえば、それらを使用して HTTP リクエスト (クライアントがサーバー上にある名前付きホストに対して行うリクエスト) をテストし、断続的な障害が発生するかどうかを確認できます。
  2. 実際の HTTP リクエストをモック オブジェクトに置き換えることができます。これにより、外部サービスの停止と完全に成功したレスポンスを予測可能な方法で偽造することができます。
  3. モッキングは、if ステートメントと except ブロックのテストが難しい場合にも役立ちます。 モック オブジェクトを使用すると、コードの実行フローを制御してそのような領域 (if と except) に到達し、コード カバレッジを改善できます。
  4. Python モック オブジェクトを使用する重要性を高めるもう 1つの理由は、コード内で対応するものをどのように使用できるかを正しく理解することです。

Python Patch() とその用途

Python のモック オブジェクト ライブラリである unittest.mock には、ターゲットを一時的にモック オブジェクトに置き換える patch() があります。 ここで、ターゲットはクラス、メソッド、または関数です。

パッチを使用するには、ターゲットを識別して patch() 関数を呼び出す方法を理解する必要があります。

ターゲットを認識するには、ターゲットがインポート可能であることを確認してから、ターゲットがどこから来たのかではなく、それが利用されている場所でターゲットにパッチを適用します。 patch() を 3つの方法で呼び出すことができます。 クラス/関数のデコレーターとして、コンテキスト マネージャーとして、または手動の開始/停止として。

patch() をクラス/関数のデコレーターとして使用するか、with ステートメント内のコンテキスト マネージャーで patch() を使用すると、ターゲットは新しいオブジェクトに置き換えられます。 どちらのシナリオでも、with ステートメントまたは関数が存在する場合、パッチは元に戻されます。

スタートアップ コードを作成し、それを addition.py ファイルに保存してみましょう。このファイルをインポートして、patch() をデコレーター、コンテキスト マネージャー、および手動の開始/停止として使用します。

起動サンプル コード (addition.py ファイルに保存):

def read_file(filename):
    with open(filename) as file:
        lines = file.readlines()
        return [float(line.strip()) for line in lines]


def calculate_sum(filename):
    numbers = read_file(filename)
    return sum(numbers)


filename = "./test.txt"
calculate_sum(filename)

test.txt の内容:

1
2
3

read_file() は filename を受け取って行を読み取り、すべての行を float 型に変換します。 これらの変換された数値のリストを返し、これをcalculate_sum()関数内のnumbers変数に保存します。

ここで、calculate_sum() は numbers リスト内のすべての数値を合計し、次のように出力として返します。

出力:

6.0

patch() をデコレータとして使用する

patch() をデコレータとして使用する方法を段階的に学びましょう。 すべてのステップの最後に、完全なソース コードを示します。

  • ライブラリとモジュールをインポートします。
    import unittest
    from unittest.mock import patch
    import addition
    

    最初に unittest ライブラリをインポートし、次に unittest.mock モジュールから patch をインポートします。 その後、起動コード addition, をインポートします。

  • test_calculate_sum() メソッドを装飾します。
    @patch('addition.read_file')
    def test_calculate_sum(self, mock_read_file):
        # ....
    

    次に、@patch デコレータを使用して test_calculate_sum() テスト メソッドを装飾します。 ここでは、additionモジュールのread_file()関数が対象です。

    @patch デコレーターを使用しているため、test_calculate_sum() には追加のパラメーター mock_read_file があります。 この追加のパラメーターは MagicMock のインスタンスです (mock_read_file の名前は任意に変更できます)。

    test_calculate_sum() 内では、patch() が addition.read_file() 関数を mock_read_file オブジェクトに置き換えます。

  • mock_read_file.return_value にリストを割り当てます。
    mock_read_file.return_value = [1, 2, 3]
    
  • calculate_sum() を呼び出してテストします。
    result = addition.calculate_sum("")
    self.assertEqual(result, 6.0)
    

    これで、calculate_sum() を呼び出し、assertEqual() を使用して、合計が 6.0 かどうかをテストできます。

    ここでは、addition.read_file() 関数の代わりに mock_read_file オブジェクトが呼び出されるため、任意の filename を calculate_sum() 関数に渡すことができます。

  • これが完全なソースコードです。

    サンプルコード (test_sum_patch_decorator.py ファイルに保存)

    import unittest
    from unittest.mock import patch
    import addition
    
    
    class TestSum(unittest.TestCase):
        @patch("addition.read_file")
        def test_calculate_sum(self, mock_read_file):
            mock_read_file.return_value = [1, 2, 3]
            result = addition.calculate_sum("")
            self.assertEqual(result, 6.0)
    

    テストを実行します。

    python -m unittest test_sum_patch_decorator -v
    

    上記のコマンドを使用してテストを実行し、次の出力を取得します。

    出力:

    test_calculate_sum (test_sum_patch_decorator.TestSum) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK
    

patch() を Context Manager として使用する

コード例 (test_sum_patch_context_manager.py ファイルに保存):

import unittest
from unittest.mock import patch
import addition


class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        with patch("addition.read_file") as mock_read_file:
            mock_read_file.return_value = [1, 2, 3]
            result = addition.calculate_sum("")
            self.assertEqual(result, 6)

このコードは、デコレータとして patch() を使用した最後のコード例に似ていますが、ここで説明するいくつかの違いがあります。

現在、 @patch('addition.read_file') コード行はありませんが、test_calculate_sum() は self パラメーター (デフォルトのパラメーター) のみを取ります。

with patch('addition.read_file') as mock_read_file は、コンテキストマネージャで mock_read_file オブジェクトを使用してパッチ addition.read_file() を意味します。

簡単に言えば、patch() は with ブロック内の addition.read_file() を mock_read_file オブジェクトに置き換えると言えます。

次に、以下のコマンドを使用してテストを実行します。

python -m unittest test_sum_patch_context_manager -v

出力:

test_calculate_sum (test_sum_patch_context_manager.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

patch() を使用して手動で開始/停止する

サンプルコード (test_sum_patch_manually.py ファイルに保存):

import unittest
from unittest.mock import patch
import addition


class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        patcher = patch("addition.read_file")
        mock_read_file = patcher.start()
        mock_read_file.return_value = [1, 2, 3]
        result = addition.calculate_sum("")
        self.assertEqual(result, 6.0)
        patcher.stop()

このコード フェンスは、前の 2つのコード例と同じことを行いますが、ここでは patch() を手動で使用します。 どうやって? 以下、理解しておきましょう。

まず、必要なライブラリをインポートします。 test_calculate_sum() 内で、patch() を呼び出して、addition モジュールのターゲット read_file() 関数でパッチを開始します。

次に、read_file() 関数のモック オブジェクトを 1つ作成します。 その後、数値のリストを mock_read_file.return_value に割り当て、calculate_sum() を呼び出して、assertEqual() を使用して 結果をテストします。

最後に、patcher オブジェクトの stop() メソッドを呼び出して、パッチ適用を停止します。 次に、次のコマンドを実行してテストします。

python -m unittest test_sum_patch_manually -v

出力:

test_calculate_sum (test_sum_patch_manually.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK