こんにちは。
本記事は株式会社インティメート・マージャーのAdvent Calendar 2024 5日目の記事になります。
突然ですが皆さん、mockしてますか?
mockしたいけどやり方がわからない。mockしようとしたけどうまくいかない、などあると思います。
そこで今回はpatch
を使ったmockの間違えやすいポイントの紹介をします。
TL;DR
patch
を使うときはimport
とfrom import
の違いを注意深く見るのが大事。
そもそもmockとは
mockとは、クラスやクラスメソッド、オブジェクトなどプログラムの一部を置き換えるものです。
置き換えた箇所がどのように使用されているかを調べたり、テストでは動いてほしくない箇所をmockで置き換えてあげることで対象の箇所を動かさないままテストを行うことができるなど大変便利な存在です。
関数の定義されたファイルを直接mockする例
以下のファイル構成で、簡単なmockの例を挙げます。
単純なクラスであれば、この例に従うことでmockができるかと思います。
.
├─ main_a.py
└─ test.py
まず、main_aをテストする例を挙げます。
def print_example():
print("example")
if __name__ == "__main__":
print_example()
import unittest
from unittest.mock import patch
import main_a
def example_mock():
print("mock")
class Tester(unittest.TestCase):
def test_main_a(self):
main_a.print_example()
with patch("main_a.print_example",side_effect=example_mock):
main_a.print_example()
unittest.main()
testを実行すると...
example
mock
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
test.pyでは、main_a.print_example()を2回呼び出しています。
ここで、一度目の呼び出しと二度目の呼び出しで出力が異なっていることが分かると思います。
正しくmockを行うことができれば、このように特定の部分を置き換えてテストを実行することができます。
とても簡単にmockをすることができました。
さて、それではどのようなところに落とし穴があるというのでしょうか?
次の例をご覧ください。
関数をimportしたファイルをmockする例
.
├─ main_a.py
├─ main_b.py
├─ main_c.py
└─ test.py
次は、先ほどのmain_aをimportして使用しているファイル、main_bとmain_cをテストする例を挙げます。
import main_a
def main():
# ここはMockしたくない
print("main_b is working!")
# ここだけMockしたい
main_a.print_example()
if __name__ == "__main__":
main()
from main_a import print_example
def main():
# ここはMockしたくない
print("main_c is working!")
# ここだけMockしたい
print_example()
if __name__ == "__main__":
main()
main_bとmain_cの間に違いはほとんどありません。main_aのimportの仕方が異なるだけです。
import unittest
from unittest.mock import patch
import main_b as main_b
import main_c as main_c
def example_mock():
print("mock")
class Tester(unittest.TestCase):
def test_main_b(self):
with patch("main_a.print_example",side_effect=example_mock):
main_b.main()
def test_main_c(self):
with patch("main_a.print_example",side_effect=example_mock):
main_c.main()
unittest.main()
testを実行すると...
func_b is working!
mock
.func_c is working!
example
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
main_bの方はmockができていますが、main_cの方はmockができておらず、そのままprint_exampleが実行されてしまっています。
エラーにもならないため、複雑なテストを書いている際にこの現象を踏んでしまうと気づき辛く大変厄介です。
どうしてこうなってしまったのか
重要なのは、 SomeClass が使われている (もしくはルックアップされている) 場所にパッチすることです。
引用先の例はクラスですが、関数である場合も同様で、import main_a
とfrom main_a import print_example
の間での、それぞれのオブジェクトを利用する時に見ている先の違いが原因でした。
def test_main_c(self):
- with patch("main_a.print_example",side_effect=example_mock):
+ with patch("main_c.print_example",side_effect=example_mock):
main_c.main()
このようにpatchを当てる先を変えることで、main_cに対してもmockをすることができます。
まとめ
本記事では、patchによるmockを行う際に間違えやすいポイントを紹介しました。
mockを行う先のファイルが、import
を使っているのかfrom import
を使っているのかを注意深く見る必要があることを理解できたかと思います。
あとがき
複雑なコードのテストを書いている時は意外とこの原因に気が付かないこともあるかと思います。そういった時にこの記事を思い出していただけると幸いです。