Python で別の関数によって呼び出される 1つの関数にモック パッチを適用する
堅牢なコードを書くときはいつでも単体テストが不可欠です。 それらは、アプリケーション ロジックを検証し、コード要件の他のすべての側面を検査するのに役立ちます。 ただし、複雑さや外部依存などのハードルにより、品質の高いテスト ケースを作成するのは困難です。
この時点で、Python モック ライブラリである unittest.mock
は、これらの障害を克服するのに役立ちます。 今日は、モック オブジェクトとその重要性、およびターゲットを一時的にモック オブジェクトに置き換える Python patch()
を使用したさまざまなサンプル コードについて学習します。
モック オブジェクトとその重要性
モック オブジェクトは、制御された方法で実際のオブジェクトの動作を模倣します。たとえば、テスト環境内でさまざまなテストを実行して、コードが期待どおりに機能することを確認します。 モック オブジェクトを使用するプロセスはモッキングと呼ばれます。 これは、テストの品質を向上させるための強力なツールです。
要点は、なぜあざける必要があるのかということです。 それを使用する根本的な理由は何ですか?
さまざまな原因が、Python でモッキングを使用することの重要性を示しています。 以下でそれらを見てみましょう。
- 主な理由の 1つは、モック オブジェクトを使用してコードの動作を改善することです。 たとえば、それらを使用して HTTP リクエスト (クライアントがサーバー上にある名前付きホストに対して行うリクエスト) をテストし、断続的な障害が発生するかどうかを確認できます。
- 実際の HTTP リクエストをモック オブジェクトに置き換えることができます。これにより、外部サービスの停止と完全に成功したレスポンスを予測可能な方法で偽造することができます。
- モッキングは、
if
ステートメントとexcept
ブロックのテストが難しい場合にも役立ちます。 モック オブジェクトを使用すると、コードの実行フローを制御してそのような領域 (if
とexcept
) に到達し、コード カバレッジを改善できます。 - 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