Python の mock.patch のハマりやすい挙動についてまとめる

`mock.patch` を使って一部のモジュールをモックに差し替えてテストをする際に、モックに変えたい箇所が思った通りに適用されずにハマったことってありませんか？
この記事では `mock.patch` を使ってモジュールをモックに差し替える時に、 patch によってどこがモックに変わるのか挙動を見てみましょう。

# TL;DR
* `import hoge` と import されているモジュールの `hoge.fuga` を patch するには `hoge.fuga` を指定する
* `from hoge import fuga` と import されている `fuga` を patch するには `<import 文を書いている方のモジュール名>.fuga` を指定する

# 動作確認用のモジュールを用意する
patch の動作を確かめるためにまずこのようなモジュールを用意します。

``` python:some_module.py
import os
from os import listdir


def some_func():
    print('os.listdir(): ', os.listdir('.'))
    print('listdir(): ', listdir('.'))
```

図で書くとこんなイメージです。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43699/3bbd84a9-b0ed-d9c0-2bfc-fb7f2d7d9b82.png)


そして `some_func()` を実行する以下のようなテストを用意します。

``` python:test.py
from some_module import some_func

class TestCase(unittest.TestCase):
    def test_some_func(self):
        some_func()
```

このテストを実行してみると以下のような出力になります。

``` bash:テストの実行結果
$ python -m unittest test.py 
os.listdir():  ['__pycache__', 'test.py', 'some_module.py']
listdir():  ['__pycache__', 'test.py', 'some_module.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
```

当然ですが `some_func()` の中で実行されている `os.listdir()`、`listdir()` はどちらも同じ結果を返していることがわかりますね。

# 1. patch による動作を確認する

`os.listdir` をモックに差し替えてテストしたい場合、対象のモジュールがどのように import されているかによって patch で指定する対象を変える必要があります。

## 1-1. `os.listdir` を patch してみる

ここでテストの内容を以下のように書き換えて `os.listdir` が `<return from mock>` という文字列を返すようにモックに差し替えてみます。

``` python:test.py
import unittest
from unittest.mock import patch

from some_module import some_func


class TestCase(unittest.TestCase):
    @patch('os.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()
```

これを実行すると以下の出力になります。

``` bash:テストの実行結果
$ python -m unittest test.py 
os.listdir():  <return from mock>
listdir():  ['__pycache__', 'test.py', 'some_module.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

`os.listdir()` の処理だけがモックに差し変わっていることがわかります。
`os.listdir` の参照先がモックに差し代わり、 `some_module.listdir` は影響を受けません。

![1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43699/bc49c371-166c-cdbe-6e43-5878fa3f640c.png)


## 1-2. `some_module.listdir` を patch してみる

今度は `some_module.listdir` を `<return from mock>` という文字列を返すモックに差し替えてみましょう。

``` python:test.py
import unittest
from unittest.mock import patch

from some_module import some_func


class TestCase(unittest.TestCase):
    @patch('some_module.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()
```

これを実行すると以下の出力になります。

``` bash:テストの実行結果
$ python -m unittest test.py 
os.listdir():  ['__pycache__', 'test.py', 'some_module.py']
listdir():  <return from mock>
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

今度は `listdir()` の処理だけがモックに差し変わっていることがわかりますね。
今度は `some_module.listdir` の参照先がモックに差し変わるので、`os.listdir` の参照先は影響を受けません。

![1-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43699/0e4f27af-2996-0c24-b7ba-235cdaf29648.png)


# 2. 複数のモジュールから利用されているモジュールを patch する

さて、今度は `os.listdir` が複数のモジュールから利用されている場合に patch の対象によってどのような挙動になるか見てみましょう。

`os.listdir` を使うモジュール `some_module.py` と `some_module_2.py` を用意します。

```python:some_module.py
import os
from os import listdir


def some_func():
    print('some_func', 'os.listdir(): ', os.listdir('.'))
    print('some_func', 'listdir(): ', listdir('.'))
```

``` python:some_module_2.py
import os
from os import listdir


def some_func_2():
    print('some_func_2', 'os.listdir(): ', os.listdir('.'))
    print('some_func_2', 'listdir(): ', listdir('.'))
```

図で書くとこんな感じです。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43699/2b9cd80b-3aac-c4ed-2145-69adec3ef434.png)


## 2-1. `os.listdir` を patch する

テストから `some_func()` `some_func_2()` の両方が実行されるようにして、 `os.listdir` を patch してみます。

``` python:test.py
import unittest
from unittest.mock import patch

from some_module import some_func
from some_module_2 import some_func_2


class TestCase(unittest.TestCase):
    @patch('os.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()
        some_func_2()
```

これを実行すると以下の出力になります。

```bash:実行結果
$ python -m unittest test.py 
some_func os.listdir():  <return from mock>
some_func listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
some_func_2 os.listdir():  <return from mock>
some_func_2 listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

`some_module` `some_module_2` の両方で `os.listdir()` の処理がモックに差し変わっていることがわかりますね。
`os.listdir` の参照先がモックに差し代わることで、`some_module` `some_module_2` の両方とも影響を受けます。

![2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43699/7c1b3e37-06cd-d532-8d36-d19da6ea9769.png)

## 2-2. `some_module.listdir` を patch する

今度は `some_module.listdir` を patch します。

```python:test.py
import unittest
from unittest.mock import patch

from some_module import some_func
from some_module_2 import some_func_2


class TestCase(unittest.TestCase):
    @patch('some_module.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()
        some_func_2()
```

これを実行すると以下の出力になります。

```bash:実行結果
$ python -m unittest test.py 
some_func os.listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
some_func listdir():  <return from mock>
some_func_2 os.listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
some_func_2 listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

今度は `some_module` 内の `listdir()` の処理だけがモックに差し変わっていることがわかります。
`some_module_2` 内の `listdir()` の処理には影響がないことが確認できます。

`some_module.listdir` の参照先だけがモックに差し変わるので、 `some_module_2.listdir` は影響を受けません。

![2-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43699/ecad7000-c7f6-9800-8642-affdc0e0467e.png)

# まとめ
これまで確認した挙動から、モックに差し替えたい対象のモジュールがどのように import されているかによって patch する対象の名前空間を変える必要があるということがわかりました。
`from hoge import fuga` としている場合は、そのモジュール内から直接 `fuga` への参照を持っていることに注意しましょう。

これまで patch の挙動をちゃんと理解してなかったのですが、この記事を読んで「なるほど！」となったので備忘のためにまとめてみました。
https://nedbatchelder.com//blog/201908/why_your_mock_doesnt_work.html

※書いてみて、 patch というよりは `import` と `from import` の挙動の違いじゃね？と思ってきましたが・・・！

patch の動きを理解して楽しくテストしよう！