Pythonでテストコードを書いていると, ときどきこんな問題に遭遇します.
- テスト対象の関数のなかで呼び出されている関数をパッチしてテストを成功させる
- 実はテスト対象の関数のなかでパッチした関数の呼び出し方が間違っていて, アプリケーションとして動かすまでそのことに気づけない
パッチするときに unittest.mock
にある patch() 関数を使いますが, このとき autospec引数にTrueを渡しておくと, この問題を回避できそうなことに気づきました.
問題のコード
実際のコードで説明します.
はじめに実装コードを示します.
def items_impl(type_):
item_table = {
"fruits": ["apple"],
"meets": ["beef"],
}
return item_table.get(type_, [])
def items():
return items_impl()
if __name__ == "__main__":
print(items())
このアプリケーションは実行時エラーが発生します.
items() 関数のなかで items_impl()関数を呼び出していますが, ここで必要な引数 type_ が指定されていないためです.
TypeError: items_impl() missing 1 required positional argument: 'type_'
次にテストコードを示します.
import unittest
from unittest import mock
import app
class AppTest(unittest.TestCase):
@mock.patch("app.items_impl", mock.MagicMock(return_value=["baseball"]))
def test_items(self):
self.assertListEqual(app.items(), ["baseball"])
if __name__ == "__main__":
unittest.main()
このテストは成功します.
items_impl() 関数をMagicMockオブジェクトでパッチしているためです.
この程度のシンプルなコードでパッチが必要になることはありませんが, それぞれの関数で評価/テストしたいときや, items_impl() 関数が未実装であったりするときに, パッチしてテストする必要があるケースはそれなりにあります.
で, 実装コードの誤りを見落としました.
patch() の autospec=True 指定で解決する
パッチするときに使う patch() 関数で autospec=Trueと引数を加えると, パッチした関数の仕様にあわない呼び出し方をすると, エラーにすることができます.
@mock.patch("app.items_impl", autospec=True)
def test_items_2(self, mock_items_impl):
mock_items_impl.return_value = ["baseball"]
self.assertListEqual(app.items(), ["baseball"])
TypeError: missing a required argument: 'type_'
(失敗/FAILEDではなくエラー. 実行時エラーなので)
次のように create_autospec() 関数と組み合わせることもできます.
@mock.patch(
"app.items_impl",
mock.create_autospec(app.items_impl, return_value=["baseball"]),
)
def test_items_3(self):
self.assertListEqual(app.items(), ["baseball"])
これで実装コードであげた問題をテストコードで検出できそうです.
参考
ドキュメントながめてて知りました.