Pythonのunittest.mock.patchパターン集（+おまけ）

この記事は [MicroAd Advent Calendar 2023](https://qiita.com/advent-calendar/2023/microad) の15日目の記事です。

単体テストを書く上で避けて通れないのが**モック**だと思います。
私自身がモック化（`patch`）に苦しめられたのでこの記事を書いています。

Pythonの標準ライブラリ`unittest`で様々な場面での`patch`のパターンを紹介します。
例は`unittest`で書きますが、`pytest`等のテストライブラリであっても同じ考え方でできるはずです。
記事の意義が問われますが[公式ドキュメント](https://docs.python.org/ja/3/library/unittest.mock.html#where-to-patch)にだいたい全部書いてあるのでそっちを読みましょう。

# パターン集

`patch`を使うときに考えるべきは名前空間です。
以下ではimportの方法でパターン分けしています。
関数とメソッド（クラス）の場合で書いていますが、考え方は同じです。

テストコードで使用するコードの基本は以下になります。
`sample.py`と`test_sample.py`の`test_main`の内容は都度変わります。

```py:test_sample.py
from unittest import TestCase
from unittest.mock import patch
import sample

class TestSample(TestCase):
    def test_main(self):
        sample.main()
```

```py:sample.py
import hoge

def main():
    hoge.fuga()
```

```py:hoge.py
def fuga():
    print("hello world")

class Fuga:
    def piyo(self):
        print("hello world")
```

このまま実行すると以下のようになり、`fuga`関数の中の`print`が実行されていることがわかります。

```sh
-> % python -m unittest discover tests/
hello world
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
```

## `import hoge`

`import hoge`の場合は`hoge`がローカルの名前空間上に束縛され、実際に`hoge.fuga`を使うときには`sample.hoge.fuga`と`hoge.fuga`を経由して「本物の`fuga`」を参照するので、経由するどちらかにパッチをあてればいいです。

### 関数の場合

このときの`sample.py`の内容は上と同じです。
以下で`fuga`をモック化できます。

```py:test_sample.py
    @patch('hoge.fuga')
    def test_main(self, mock):
        sample.main()
```

printされておらず、モック化できたことがわかります。

```sh
-> % python -m unittest discover tests/ 
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
```

以下でもモック化が可能です。

```py:test_sample.py
    @patch('sample.hoge.fuga')
```

### メソッド（クラス）の場合

```py:sample.py
import hoge

def main():
    hoge.Fuga().fuga()
```

以下のどちらかで`fuga`をモック化できます。

```py:test_sample.py
    @patch('hoge.Fuga.fuga')
```

```py:test_sample.py
    @patch('sample.hoge.Fuga.fuga')
```

クラスごとモック化もできます。

```py:test_sample.py
    @patch('hoge.Fuga')
```

```py:test_sample.py
    @patch('sample.hoge.Fuga')
```

## `import hoge as piyo`

`import hoge as piyo`の場合は`piyo`がローカルの名前空間上に束縛され、実際に`piyo.fuga`を使うときには`sample.piyo.fuga`と`hoge.fuga`を経由して「本物の`fuga`」を参照するので、経由するどちらかにパッチをあてればいいです。

### 関数の場合

```py:sample.py
import hoge as piyo

def main():
    piyo.fuga()
```

以下のどちらかで`fuga`をモック化できます。

```py:test_sample.py
    @patch('hoge.fuga')
```

```py:test_sample.py
    @patch('sample.piyo.fuga')
```

### メソッド（クラス）の場合

```py:sample.py
import hoge as piyo

def main():
    piyo.Fuga().fuga()
```

以下のどちらかで`fuga`をモック化できます。

```py:test_sample.py
    @patch('hoge.Fuga.fuga')
```

```py:test_sample.py
    @patch('sample.piyo.Fuga.fuga')
```

クラスごとモック化もできます。

```py:test_sample.py
    @patch('hoge.Fuga')
```

```py:test_sample.py
    @patch('sample.piyo.Fuga')
```

## `from hoge import fuga`

`from hoge import fuga`の場合は`fuga`がローカルの名前空間上に束縛され、実際に`fuga`を使うときには`sample.fuga`を経由して「本物の`fuga`」を参照するので、そこにパッチをあてればいいです。
このとき、`import`パターンで経由していた`hoge.fuga`はないので、そこに対してパッチをあててもうまくいきません。

### 関数の場合

```py:sample.py
from hoge import fuga

def main():
    fuga()
```

以下で`fuga`をモック化できます。

```py:test_sample.py
    @patch('sample.fuga')
```

### メソッド（クラス）の場合

```py:sample.py
from hoge import Fuga

def main():
    Fuga().fuga()
```

以下で`fuga`をモック化できます。

```py:test_sample.py
    @patch('sample.Fuga.fuga')
```

クラスごとモック化もできます。

```py:test_sample.py
    @patch('sample.Fuga')
```

## `from hoge import fuga as piyo`

`from hoge import fuga as piyo`の場合は`piyo`がローカルの名前空間上に束縛され、実際に`piyo`を使うときには`sample.piyo`を経由して「本物の`fuga`」を参照するので、そこにパッチをあてればいいです。
このときも`hoge.fuga`を経由しないので、そこに対してパッチをあててもうまくいきません。

### 関数の場合

```py:sample.py
from hoge import fuga as piyo

def main():
    piyo()
```

以下で`fuga`をモック化できます。

```py:test_sample.py
    @patch('sample.piyo')
```

### メソッド（クラス）の場合

```py:sample.py
from hoge import Fuga as Piyo

def main():
    Piyo().fuga()
```

以下で`fuga`をモック化できます。

```py:test_sample.py
    @patch('sample.Piyo.fuga')
```

クラスごとモック化もできます。

```py:test_sample.py
    @patch('sample.Piyo')
```

## 組み込み関数・型

組み込み関数・型（仮に`print`とします）の場合は`print`がローカルの名前空間上に束縛され、実際に`print`を使うときには`sample.print`と`builtins.print`を経由して「本物の`print`」を参照するので、経由するどちらかにパッチをあてればいいです。

### 関数の場合

```py:sample.py
def main():
    print("hello world")
```

以下のどちらかで`print`をモック化できます。

```py:test_sample.py
    @patch('sample.print')
```

```py:test_sample.py
    @patch('builtins.print')
```

### 型の場合

```py:sample.py
def main():
    print(list("hello world"))
```

以下のどちらかで`list`をモック化できます。

```py:test_sample.py
    @patch('sample.list')
```

```py:test_sample.py
    @patch('builtins.list')
```

# おまけ

## `cannot set 'fuga' attribute of immutable type 'hoge'`

たとえば`datetime.now`をモック化して常に同じ時刻を返したい、みたいな場面があったとします。
`freezegun`等の専用のライブラリもありますが、ここでは使わない場合を考えます。
ちなみにこれも[公式ドキュメント](https://docs.python.org/ja/3/library/unittest.mock-examples.html#partial-mocking)にあります。

```py:sample.py
from datetime import datetime

def main():
    print(datetime.now())
```

ストレートに行くと以下でモック化しようとするでしょう。
しかしこれだとエラーが発生します。

```py:test_sample.py
    @patch('sample.datetime.now')
```

```sh
TypeError: cannot set 'now' attribute of immutable type 'datetime.datetime'
```

解決策として`datetime`自体をモック化する方法が出てきます。

```py:test_sample.py
    @patch('sample.datetime')
    def test_main(self, mock):
        from datetime import datetime
        mock.now.return_value = datetime(2023, 1, 2, 3)
        sample.main()
```

これはクラス全体をモック化してしまい、以下のように`now`以外も使っている場面では`datetime()`の呼び出しもモック化されてしまい不適切です。

```py:sample.py
from datetime import datetime

def main():
    print(datetime.now())
    print(datetime(2023, 12, 15, 7))
```

```sh
-> % python -m unittest discover tests/
2023-01-02 03:00:00
<MagicMock name='datetime()' id='4380121248'>
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

使用する全てのメソッドの返す値を記述する、もしくは以下の方法で解消できます。

```py:test_sample.py
        mock.side_effect = lambda *args, **kw: datetime(*args, **kw)
```