2021.05.15   2021.09.19

【Python】unittest.mock.patchの使い方

Python    


この記事では、Pythonのunittestで使用するpatchの使い方について解説します。

後半では、side_effectを使用したテストや、patchを複数記述する方法についても紹介します。


    目次
  1. patchとは
  2. patchの使い方(基本)
  3. patchの使い方(複数の戻り値を設定してテストを行う) -- side_effect --
  4. patchの使い方(複数のpatchを定義する方法)
  5. 別パターンのpatchの書き方

patchとは

単体テスト(unittest)のモジュールのひとつ。

テスト対象のプログラムの中で呼び出される関数について、未完成などの理由から取得したい戻り値が得られない場合に、想定される戻り値をテスト実施者が設定できるモジュールのことです。

参考: unittest.mock --- 入門

patchの使い方(基本)

プロジェクトの準備

まずはこのようなプロジェクトを用意します。

calc_project
├── src
│   └── calc.py
└── test
    └── test_calc.py


テスト対象のソース

テスト対象のソース「calc.py」には、以下のようなクラスと関数が定義されいます。

calc.py
class calc:
    # xとyを足した後に2倍して1を足す関数
   @staticmethod
    def x_y_addition_and_twice(x, y):
        z = x + y
        # 足し算した結果を2倍する関数「twice」はまだ完成していない
        tmp = calc.twice(z)
        result = tmp + 1
        return result

    # まだ完成していない関数
    @staticmethod
    def twice(z):
        return "未完成の関数です"

今回テストするのは、xとyを足した後に2倍して1を足す関数である「def x_y_addition_and_twice(x, y)」です。

しかし、この関数内で定義されいるtwice関数はまだ未完成のため、足し算した結果を2倍にしてくれません。

こういった場合、patchの出番となる訳です。


test_calc.py のテストコード

まずは、patchを書かずにテストコードを記述します。

test_calc.py
import unittest
from calc_project.src.calc import calc


class MyTestCase(unittest.TestCase):

    def test_calc_normal_case_1(self):

        # x_y_addition_and_twice関数のテストを実行
        result = calc.x_y_addition_and_twice(1, 1)
        # 1 + 1 を 2倍して1を足す処理なので期待値は5
        self.assertEqual(5, result)


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

result = calc.x_y_addition_and_twice(1, 1) がテスト対象の関数を実行している部分です。

引数に(1, 1)を指定しているので、1 + 1 の 2倍足す1である 5がこの関数の期待値です。

しかし、前述した通り、値を2倍にする関数 twice は未完成であるため、実行するとエラーとなります。

実行結果
Ran 1 test in 0.003s

FAILED (errors=1)

Error


test_calc.py にpatchを使用する

では、先ほどのコードに@patchを定義します。

test_calc.py
import unittest
from unittest.mock import patch  # patchをimportする
from calc_project.src.calc import calc


class MyTestCase(unittest.TestCase):

    @patch("calc_project.src.calc.calc.twice")  # patchを定義
    def test_calc_normal_case_1(self, twice_patch):  # 上記patchオブジェクトを引数に渡す
        twice_patch.return_value = 4  # patchオブジェクトに戻り値を設定

        result = calc.x_y_addition_and_twice(1, 1)
        self.assertEqual(5, result)


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


from unittest.mock import patch

まず、2行目でpatchを使用するために、インポートを行なっています。

@patch("calc_project.src.calc.calc.twice")

7行目ではpatchのデコレーターを定義しています。

引数にはpatchをあてる関数(今回は twice )までのパスを記述します。

def test_calc_normal_case_1(self, twice_patch): 

8行目で@patchで生成されたオブジェクトを関数の引数としてtwice_patchという名前で渡しています。

引数の名前はtwice_patchではなく、何を設定しても問題ありません。

twice_patch.return_value = 4 

9行目でtwice関数の戻り値を設定しています。

先ほど引数に設定した twice_patch オブジェクトの変数 return_value (patch化すると使用できる)に任意の値を渡すことで設定することができます。

patchでインスタンス化したオブジェクトについてはすべて return_value(関数の戻り値) を設定することができます。

twice関数の戻り値は、1 + 1 の2倍である4が期待されるので、ここでは4を設定しています。

result = calc.x_y_addition_and_twice(1, 1)
self.assertEqual(5, result)

11行目で、テストする関数を実行しています。
その後、12行目でテストした関数の戻り値を確認しています。

1 + 1 を2倍して、さらに1を足した値が期待値なので、第1引数に5を設定しています。

assertEqualは第一引数と第二引数の値が等しいかどうかチェックするテスト関数です。

ここまで記述して再度テストを実行すると無事にテストがOKとなります。

Ran 1 test in 0.002s

OK

複数の戻り値を設定してテストを行う -- side_effect --

先ほどの例では、patchオブジェクトの戻り値を以下のように記述していました。

 twice_patch.return_value = 4

しかし、この書き方では、モックにした関数1つに対して1つの戻り値しか設定できず、モックに複数の値を設定した場合のテストがおこなえません。

そのような場合は「return_value」をしようせず「side_effect」を利用すると便利です。

side_effect には iterable を設定することができ、テスト対象のコードを実行するたびに、違う値がモックに設定されるようなテストを行えます。

calc.py (テスト対象コード)
class calc:
    # xとyを足した後に2倍したあとプラス1する関数
    @staticmethod
    def x_y_addition_and_twice(x, y):
        z = x + y
        tmp = calc.twice(z)
        result = tmp + 1
        return result

    @staticmethod
    def twice(z):
        return "未実装の関数です"
test_calc.py (テスト実施コード)
class MyTestCase(unittest.TestCase):

    @patch("calc_project.src.calc.calc.twice")
    def test_calc_normal_case_1(self, twice_patch):
        twice_patch.side_effect = [4, 6, 8]  # side_effectの設定

        # テストケース1   
        result = calc.x_y_addition_and_twice(1, 1)
        self.assertEqual(5, result)
        # テストケース2   
        result = calc.x_y_addition_and_twice(1, 2)
        self.assertEqual(7, result)
        # テストケース3   
        result = calc.x_y_addition_and_twice(2, 2)
        self.assertEqual(9, result)


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


test_calc.py についての解説

twice_patch.side_effect = [4, 6, 8]

5行目でside_effectを設定しています。

テストのケース順にtwice関数の期待値をリストで設定します。

# テストケース1   
result = calc.x_y_addition_and_twice(1, 1)
self.assertEqual(5, result)
# テストケース2   
result = calc.x_y_addition_and_twice(1, 2)
self.assertEqual(7, result)
# テストケース3   
result = calc.x_y_addition_and_twice(2, 2)
self.assertEqual(9, result)

7行目以降でcalc.x_y_addition_and_twice関数のテストを行なっています。

それぞれ、引数に設定した値に対して想定した値が返ってきているかを確認しています。

calc.x_y_addition_and_twice(1, 1)の場合は、

足した結果が2となり、twiceで2倍にすると4になります。

つまり、1回目にテストするコードのモックtwiceの想定結果はside_effectの1番目の要素に設定します。

そこからさらにプラス1されるので、テストケース1のresultには5が返ってくる想定となります。

calc.x_y_addition_and_twice(1, 2)の場合は、

足した結果が3となり、twiceで2倍にすると6になります。

つまり、2回目にテストするコードのモックtwiceの想定結果は6となるので、side_effectの2番目の要素に6を設定します。

それ以降も同様にケースを複数作成できます。

実際にテストコードを実行すると次のようになります。

Ran 1 test in 0.003s

OK

無事テストコードが通りました。

複数のpatchを定義する方法

patchを1つだけではなく、複数設定したい場合があると思います。

その際は、@patchデコレーターを重ねて記述すると、patchを複数利用できます。

下記の例では、計算結果を半分にする関数halfを新たにpatchとして定義しています。

    @patch("calc_project.src.calc.calc.half")  # 新たにhalf関数をpatchにする
    @patch("calc_project.src.calc.calc.twice")  
    def test_calc_normal_case_1(self, twice_patch, half_patch):  # 引数 half_patch を追加

注意点としては、テスト関数に与えるpatchオブジェクトの引数は、テスト関数に近い順番で与えないといけません。

上記の例では、twiceのpatchは第1引数halfのpatchは第2引数に指定しています。

もし次のようにhoge関数を追加した場合は次のような記述となります。

    @patch("calc_project.src.calc.calc.hoge")  # 新たにhoge関数をpatchにする
    @patch("calc_project.src.calc.calc.half")  
    @patch("calc_project.src.calc.calc.twice")  
    def test_calc_normal_case_1(self, twice_patch, half_patch, hoge_patch):  # 引数 hoge_patch を追加

さらにpatchを増やす場合も同様に設定を行います。

以下は複数のpatchを使用したテストの記述例です。

calc.py
class calc:
    # xとyを足した後に2倍して半分にする関数
    @staticmethod
    def x_y_addition_and_twice_half(x, y):
        z = x + y
        tmp = calc.twice(z)
        # twice関数の結果を半分にする処理を追加。まだ完成していない
        result = calc.half(tmp)
        return result

    @staticmethod
    def twice(z):
        return "未実装の関数です"

    # half関数を追加
    @staticmethod
    def half(tmp):
        return "未実装の関数です"
test_calc.py
import unittest
from unittest.mock import patch
from calc_project.src.calc import calc


class MyTestCase(unittest.TestCase):

    @patch("calc_project.src.calc.calc.half")  # 2つ目のpatchを定義
    @patch("calc_project.src.calc.calc.twice")
    def test_calc_normal_case_1(self, twice_patch, half_patch):  # 第2引数にhalf_patchを定義
        twice_patch.return_value = 4
        half_patch.return_value = 2  # 2つ目のモックに戻り値を設定

        result = calc.x_y_addition_and_twice_half(1, 1)
        self.assertEqual(2, result)


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

別パターンのpatchの書き方

今までのpatchの書き方は、テスト関数にpatchのオブジェクトを渡し、そのオブジェクトの変数に戻り値を設定するというものでした。

ただ、それは次のように書き換えて使うこともできます。
参考:patch デコレータ

import unittest
from unittest.mock import patch
from calc_project.src.calc import calc


class MyTestCase(unittest.TestCase):

    def mock_example_falf(self):  # falf関数のモックと戻り値を定義
        return 2

    def mock_example_twice(self):  #  twice関数のモックと戻り値を定義
        return 4

    @patch("calc_project.src.calc.calc.half", mock_example_falf)  # 第2引数にモックを設定
    @patch("calc_project.src.calc.calc.twice", mock_example_twice)  # 第2引数にモックを設定
    def test_calc_normal_case_1(self,):

        result = calc.x_y_addition_and_twice_half(1, 1)
        self.assertEqual(2, result)


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

相違点は、処理の始めにモックを作成し、 @patchの第2引数でそのモックを渡しているところです。

そうすると、前回記述していた twice_patch.return_value = 4 といった return_valueの記述は不要となります。

このテストコードも、先ほど同様の結果となります。

Ran 1 test in 0.002s

OK

参考資料: https://docs.python.org/ja/3/library/unittest.mock-examples.html

コメント
現在コメントはありません。
コメントする
コメント入力

名前 (※ 必須)

メールアドレス (※ 必須 画面には表示されません)

送信