この記事では、Pythonのunittestで使用するpatchの使い方について解説します。
後半では、side_effectを使用したテストや、patchを複数記述する方法についても紹介します。
- patchとは
- patchの使い方(基本)
- patchの使い方(複数の戻り値を設定してテストを行う) -- side_effect --
- patchの使い方(複数のpatchを定義する方法)
- 別パターンのpatchの書き方
patchとは
単体テスト(unittest)のモジュールのひとつ。
テスト対象のプログラムの中で呼び出される関数について、未完成などの理由から取得したい戻り値が得られない場合に、想定される戻り値をテスト実施者が設定できるモジュールのことです。
patchの使い方(基本)
プロジェクトの準備
まずはこのようなプロジェクトを用意します。
calc_project
├── src
│ └── calc.py
└── test
└── test_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を書かずにテストコードを記述します。
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を定義します。
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 を設定することができ、テスト対象のコードを実行するたびに、違う値がモックに設定されるようなテストを行えます。
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 "未実装の関数です"
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を使用したテストの記述例です。
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 "未実装の関数です"
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