【Python】Patchを使ったMockのハマりポイント

こんにちは。

本記事は株式会社インティメート・マージャーの[Advent Calendar 2024](https://qiita.com/advent-calendar/2024/intimatemerger) 5日目の記事になります。

突然ですが皆さん、mockしてますか？
mockしたいけどやり方がわからない。mockしようとしたけどうまくいかない、などあると思います。

そこで今回は`patch`を使ったmockの間違えやすいポイントの紹介をします。

# TL;DR
`patch`を使うときは`import`と`from import`の違いを注意深く見るのが大事。

# そもそもmockとは
mockとは、クラスやクラスメソッド、オブジェクトなどプログラムの一部を置き換えるものです。
置き換えた箇所がどのように使用されているかを調べたり、テストでは動いてほしくない箇所をmockで置き換えてあげることで対象の箇所を動かさないままテストを行うことができるなど大変便利な存在です。

[Pythonのunittest.mockのドキュメント](https://docs.python.org/ja/3/library/unittest.mock.html)

# 関数の定義されたファイルを直接mockする例
以下のファイル構成で、簡単なmockの例を挙げます。
単純なクラスであれば、この例に従うことでmockができるかと思います。

```:ファイル構成
.
├─ main_a.py
└─ test.py
```
まず、main_aをテストする例を挙げます。

```python:main_a.py
def print_example():
    print("example")

if __name__ == "__main__":
    print_example()
```

```python:test.py
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を実行すると...

```:test.pyの実行結果
example
mock
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

test.pyでは、main_a.print_example()を２回呼び出しています。
ここで、一度目の呼び出しと二度目の呼び出しで出力が異なっていることが分かると思います。
正しくmockを行うことができれば、このように特定の部分を置き換えてテストを実行することができます。
とても簡単にmockをすることができました。

さて、それではどのようなところに落とし穴があるというのでしょうか？
次の例をご覧ください。

# 関数をimportしたファイルをmockする例
```:ファイル構成
.
├─ main_a.py
├─ main_b.py
├─ main_c.py
└─ test.py
```
次は、先ほどのmain_aをimportして使用しているファイル、main_bとmain_cをテストする例を挙げます。

```python:main_b
import main_a


def main():
    # ここはMockしたくない
    print("main_b is working!")
    # ここだけMockしたい
    main_a.print_example()


if __name__ == "__main__":
    main()
```

```python:main_c
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の仕方が異なるだけです。

```python:test
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を実行すると...

```:test.pyの実行結果
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が実行されてしまっています。
エラーにもならないため、複雑なテストを書いている際にこの現象を踏んでしまうと気づき辛く大変厄介です。

# どうしてこうなってしまったのか
[mockのパッチ先についてのドキュメント](https://docs.python.org/ja/3/library/unittest.mock.html#id6)
>重要なのは、 SomeClass が使われている (もしくはルックアップされている) 場所にパッチすることです。

引用先の例はクラスですが、関数である場合も同様で、`import main_a`と`from main_a import print_example`の間での、それぞれのオブジェクトを利用する時に見ている先の違いが原因でした。
```diff_python
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`を使っているのかを注意深く見る必要があることを理解できたかと思います。

# あとがき
複雑なコードのテストを書いている時は意外とこの原因に気が付かないこともあるかと思います。そういった時にこの記事を思い出していただけると幸いです。