Python の unittest の mock

# 概要

Python の unittest を使っていて、mock が何をするものかは分かっているけど、まだちょっと得体が知れない、怖い、という段階があると思います。
この段階を克服するために、何も知らない状態から徐々に mock を理解するためのステップを作りたいと思いました。

対象は Python 3.x です。

# どんな時にモックしたいか

単体テストでは、個々の機能を切り離してテストしたいものです。

たとえば A という関数をテストしたい時、A の中で B という関数を呼んでいるとします。
テストしたいのは A の機能なので、B にバグがあっても A にバグがなければテストはパスさせたいです。
また、このテストの期待結果を決めるのに、B からの返り値が予想または操作できると嬉しいです。
![mock_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/6af61f2d-1291-6b71-8e0c-4f261f95eee6.png)
そこで、テスト中だけは B を呼ぶ代わりに、期待通りに動く操り人形のようなものを呼ぶようにします。
この操り人形を B のモックといいます。
また、このように呼び出し先をすげ替えることを「モックする」と言ったりします。
![mock_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/4d3c31da-c427-ba5e-78ff-af5a4355e8ad.png)

# 具体例

具体例として、チケット購入サイトにあるような「空席状況」を表示するプログラムを考えます。
といっても難しいことは無くて、残席数によって「完売」「残り僅か」「余裕あり」のいずれかを返すだけです。

イメージしやすいように `Event` というクラスを作って、空席状況を返す関数 (関数 A) を定義しましょう。

```python:my_class.py
from my_func import get_available_seats

class Event:
    '''イベント
    '''
    def __init__(self, id: int):
        self.id = id

    def check_availability(self):
        '''空席状況を返す関数 (関数 A)
        '''
        # 残席数と座席数を取得する
        available_seats = get_available_seats(self.id)

        if available_seats == 0:
            return '完売'
        elif available_seats < 20:
            return '残り僅か'
        else:
            return '余裕あり'
```

残席数 (`available_seats`) は、別モジュールの関数 (関数 B) を呼んで取得しています。

```python:my_func.py
import random

def get_available_seats(event_id: int) -> int:
    '''残席数を返す関数 (関数 B)
    '''
    # 残席数 (本来はデータベースに問い合わせるが、ここでは乱数)
    available_seats = random.randint(0, 100)

    return available_seats
```

このような情報はふつうはデータベースに問い合わせて取得しますが、話を簡単にするために乱数にしています。
なので `event_id` (イベント ID) 引数を使っていませんが、関数の役割をイメージしやすいように引数を残してあります。

これらのモジュールは同じフォルダにあるものとし、パッケージ化していません。

```
./
 ├─ my_class.py
 └─ my_func.py
```

## 動作確認

動作を見るには同じフォルダから以下のようなプログラムを実行します。

```python:main.py
from my_class import Event

# Event を作る
event = Event(id=1)

# 空席状況を取得する
availability = event.check_availability()

print(availability)
```

出力はこのようになります。

```
> python ./main.py
余裕あり
```

# ふつうにテストを書いてみる

関数を呼んで期待した結果が返ってくることを確認する、という「ふつうのテスト」を unittest で書いてみます。

```python:tests/test_00.py
from unittest import TestCase
from my_class import Event

class EventTest(TestCase):
    def test_availability(self):
        # Event を作る
        event = Event(id=1)

        # 空席状況を取得する
        availability = event.check_availability()

        # いずれかの空席状況が返ってきたことを確認する
        self.assertIn(availability, ['完売', '残り僅か', '余裕あり'])
```

テストの実行は以下のようにします (tests フォルダの中にテストコードを書いた場合)。

```
> python -m unittest tests.test_00
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
```

:::note warn
テストコードから my_class モジュールをインポートしているので、上記のコマンドは my_class モジュールのあるフォルダで実行する必要があります。
また、上記のように `tests.test_00` で実行する場合は tests フォルダがパッケージになっている (\_\_init__.py を含む) 必要があります。
:::

## このテストの問題点

このテストの問題点は、関数 A の「空席状況を残席数に基づいて判定する」という機能をテストできていないことです。
その機能を確認するには、テスト中に残席数 (関数 B の返り値) を知る必要がありますが、それはテストコードからは見えません。

また、仮にその情報を得られたとしても、その時どうだったかだけでなく「残席数が〇の場合は？」という気になるパターンについても確認したいです。

これがモックを使いたい状況です。

# 自力でモックしてみる

モックを理解する最初のステップとして、自力でモックを作ってみましょう。

最初に書いたように、モックは関数 B をすげ替えるものです。
関数 A が `get_available_seats()` を呼んだ時に、モックが呼ばれるようにすれば良いのです。

```python:tests/test_01.py
from unittest import TestCase
import my_class
from my_class import Event

class EventTest(TestCase):
    def test_availability(self):
        # Event を作る
        event = Event(id=1)

        # モック用の関数 (必ず 0 を返す)
        def mock_func(id):
            return 0

        # 関数 B をモック用の関数にすげ替える
        my_class.get_available_seats = mock_func

        # 空席状況を取得する
        availability = event.check_availability()

        # 空席状況が「完売」であることを確認する
        self.assertEqual(availability, '完売')
```

何をやっているかというと、必ず `0` を返す関数を作って、それを `my_class.get_available_seats` に代入して (すげ替えて) います。

「関数に関数を代入する」ということの意味が分からない方は、「関数も Python のオブジェクトである」ということと、「オブジェクトの名前はその実体を指すポインタである」ということをイメージしてみてください。

::: note
参考
- [Pythonの型とオブジェクト (Python.jp)](https://www.python.jp/train/list/index.html)
- [名前とオブジェクトについて (python.org)](https://docs.python.org/ja/3/tutorial/classes.html#a-word-about-names-and-objects)
:::

これで、「残席数が 0 だった場合」のテストはできるようになりました。

## すげ替える時の注意点

上記の自力のモックでは、`my_class` モジュールをインポートして、その `get_available_seats` 属性を変更して (すげ替えて) います。

これ以外の、たとえば以下のような方法では上手くいきません。

```python:関数のすげ替えが上手くいかない例
from my_class import get_available_seats

class EventTest(TestCase):
    def test_availability(self):
        # (中略)
        get_available_seats = mock_func
```

先ほどの例とは `import` の書き方が違います。
上手くいかない理由は `import` で何が起きているかを知れば分かります。

### import で起きていること

`import` で起きていることについて、少し説明します。
ひと言でいうと、別の名前空間にある変数を、今いる名前空間にコピーしています。
ただしコピーされるのはポインタ、つまり「何を指しているか」という情報です。

たとえば、先ほどの上手くいかないパターンでは以下のことが起きています。
ステップ・バイ・ステップで見てみましょう。

**1. 元の関数 B の定義**

まずは `import` の前に `def` で関数を定義した時、関数の実体とそれを呼ぶための名前が作られます。
![import1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/09699c47-f057-ae32-d7ce-3825aa18836a.png)
**2. my_class に import**

関数 (の名前) を `my_class` モジュールに `import` すると、その名前空間にコピーが作られます。
コピー元と同じ関数を指しますが、あくまでコピーです。
関数 A は、このコピーの方を使って関数 B を呼びます。
![import2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/5c6fac90-898b-cca2-c349-77de06b984ce.png)
**3. テストコードに import**

これをさらにテストコードに `import` すると以下のようになります。
関数 A が呼んでいるものをすげ替えたいので、これで良いようにも思えますが…。
![import3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/a30ef168-b587-f084-fdb5-a5a22d551632.png)
**4. モックすると…**

なんと、これをモック用の関数にすげ替えても、関数 A から呼ばれる関数 B は変わりません。
テストコードに `import` したものは、その時点でコピーになっていたからです。
![import4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/34c14baa-0395-546d-84f9-a2593f8c3a82.png)
テストをしている名前空間は tests.test01 ですが、関数 A は相変わらず my_class にいるので、そっちの方を書き換えないといけなかったのです。

### 正しい import とモックのしかた

正しくモックするには、「自力でモックしてみる」の最初のコードのように `my_class` モジュールを `import` します。
すると、`my_class` という「モジュールを指すポインタ」がコピーされます。
![import5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/466a9569-ad60-1df8-b7c9-24e6b3fd230e.png)
これを使って `my_class.get_available_seats` と書けば、`my_class` モジュールの中の `get_available_seats` (コピーでなくそのもの) にアクセスできます。
![import6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/db6b3c58-b343-89af-e2ca-eb2667809745.png)
モック用の関数をそこに代入すると以下のようになり、すげ替えが上手くいきます。
![import7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/dbda2a50-112c-d8a6-0b9c-b0acbda0565d.png)

::: note
何を `import` してモックするべきかという話題については、後ほど説明する "patcher" に関する公式ドキュメントに説明があります。

- [patcher - どこにパッチするか (python.org)](https://docs.python.org/ja/3/library/unittest.mock.html#where-to-patch)
:::

# unittest の mock

公式ドキュメントの [unittest.mock のページ](https://docs.python.org/ja/3/library/unittest.mock.html) を見ると、いきなり以下のコードが載っていて取っつきづらいと思います。
頑張ってこれを理解するのではなく、先ほどの例を使って mock の使い方を見ていきましょう。

```python:公式ドキュメントにあるサンプル
>>> from unittest.mock import MagicMock
>>> thing = ProductionClass()
>>> thing.method = MagicMock(return_value=3)
>>> thing.method(3, 4, 5, key='value')
3
>>> thing.method.assert_called_with(3, 4, 5, key='value')
```

少し説明すると、3行目で [`MagicMock` クラス](https://docs.python.org/ja/3/library/unittest.mock.html#magic-mock) のオブジェクトを作って、`thing.method` という関数をそれにすげ替えています。
ここからは先ほど自力でやったのと同じことを、この `MagickMock` クラスでやってみます。

::: note
「関数は関数で置き換えるべきなのでは」と思うかも知れませんが、Python ではオブジェクトも関数のように「呼び出せる」ようにできます。
見方を変えて、関数が[「呼び出し可能オブジェクト」](https://docs.python.org/ja/3/reference/datamodel.html?highlight=__call__#object.__call__:~:text=%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E5%8F%AF%E8%83%BD%E5%9E%8B%20(callable%20type))の一種だと考えても良いです。

- [6.3.4. 呼び出し (call) (python.org)](https://docs.python.org/ja/3/reference/expressions.html#calls)
- [Mock クラス - 呼び出し (python.org)](https://docs.python.org/ja/3/library/unittest.mock.html#calling)
:::

# MagicMock を使ってみる

先ほどの空席状況の例で `MagicMock` を使った場合の書き方は、このようになります。

```python:tests/test_02.py
from unittest import TestCase
from unittest.mock import MagicMock
import my_class
from my_class import Event

class EventTest(TestCase):
    def test_availability(self):
        # Event を作る
        event = Event(id=1)

        # 関数 B を MagicMock オブジェクトにすげ替える
        my_class.get_available_seats = MagicMock(return_value=0)

        # 空席状況を取得する
        availability = event.check_availability()

        # 空席状況が「完売」であることを確認する
        self.assertEqual(availability, '完売')
```

`MagicMock` オブジェクトに `return_value` という属性を設定すると、それが関数として呼び出された時に何を返すかを決められます。

`return_value` は属性なので、オブジェクトを作った後で設定したり変更したりできます。

```python:tests/test_03.py
class EventTest(TestCase):
    def test_availability(self):
        # Event を作る
        event = Event(id=1)

        # 関数 B を MagicMock オブジェクトにすげ替える
        my_class.get_available_seats = MagicMock()

        # 残席数が 0 なら空席状況が「完売」となること
        my_class.get_available_seats.return_value = 0
        availability = event.check_availability()
        self.assertEqual(availability, '完売')

        # 残席数が 19 なら空席状況が「残り僅か」となること
        my_class.get_available_seats.return_value = 19
        availability = event.check_availability()
        self.assertEqual(availability, '残り僅か')
```

## どうなっているのか

使い方は分かっても、この「属性を設定するだけで返り値が変わる」という動作が気持ち悪いと、この先の理解がスムーズにいかないかも知れません。
蛇足かもしれませんが、これがどういう仕組みなのかを想像するために、自力でモックオブジェクトを作ってみます。

```python:my_mock.py
class MyMock:
    def __init__(self, return_value=None):
        self.return_value = return_value

    def __call__(self, *args, **kwargs):
        return self.return_value
```

この `__call__` というのが、上で補足説明に書いた「呼び出し可能オブジェクト」にするための特殊な関数名になっています (これは Python のルールです)。
先ほどのコードくらいであれば、`MagicMock` をこの `MyMock` で置き換えても動きます。

実際の `MagicMock` はこんなに単純ではないので、`MyMock` はあくまでモックオブジェクトがどうなっているのかの理解のためと思ってください。

::: note
Mock クラスと MagicMock クラス
Python の unittest の mock の説明で、`MagicMock` 以外に [`Mock` クラス](https://docs.python.org/ja/3/library/unittest.mock.html#the-mock-class) というのが出てくることがありますが、これはあまり気にしなくても大丈夫だと思います。
`MagicMock` が `Mock` の拡張版なのですが、あえて `Mock` を使う理由はあまり無いですし、この後の "patcher" を使う方法では自動的に `MagicMock` が使われます。
ただしドキュメントを読む時に `MagicMock` の継承元である `Mock` の方を読む必要はあります。
:::

# モックを元に戻すには

関数にモックオブジェクトを代入してすげ替える方法を見てきました。
テスト中にこれを元に戻したい場合は、すげ替える前に元の関数を退避しておけば可能です。
このことは「代入によってポインタがコピーされる」という理屈から分かると思います。

```python:モックを元に戻す例
# 元の関数を退避する
original_func = my_class.get_available_seats

# モックする
my_class.get_available_seats = MagicMock()

# (この関数が呼ばれるようなテスト)

# モックを元に戻す
my_class.get_available_seats = original_func
```

patcher を使うともっと簡単にできます。

## patcher で一時的にモックする

patcher は、あるスコープ内だけで一時的にモックを適用するためのもの (という概念) です。
patcher の実装は unittest では [`patch` 関数](https://docs.python.org/ja/3/library/unittest.mock.html#patch) で、コンテキストマネジャやデコレータとして使えます。

### コンテキストマネジャとして使う

まずはコンテキストマネジャとして [`with` 文](https://docs.python.org/ja/3/reference/compound_stmts.html#the-with-statement) で使う方法を見てみます。

```python:tests/test_05.py
from unittest import TestCase
from unittest.mock import patch
from my_class import Event

class EventTest(TestCase):
    def test_availability(self):
        # Event を作る
        event = Event(id=1)

        with patch('my_class.get_available_seats') as mock_func:
            # 残席数が 0 の場合は「完売」となること
            mock_func.return_value = 0
            availability = event.check_availability()
            self.assertEqual(availability, '完売')

            # 残席数が 19 の場合は「残り僅か」となること
            mock_func.return_value = 19
            availability = event.check_availability()
            self.assertEqual(availability, '残り僅か')
```

`patch` 関数の第1引数は、モックしたいものの名前を (名前空間を含め) 文字列にしたものです。
この書き方であれば、前述の「すげ替える時の注意点」のようなことはあまり考えなくて済みます。

`with` ブロックを出ると関数はモックする前の状態に戻りますが、モックオブジェクト (`mock_func`) は残るので、モック中に得た情報を確認することができます。

```python:モック中に得た情報を確認する例
class EventTest(TestCase):
    def test_availability(self):
        # (中略)
        with patch('my_class.get_available_seats') as mock_func:
            # (中略)

        # my_class.get_available_seats が2回呼ばれたことを確認する
        self.assertEqual(mock_func.call_count, 2)
```

::: note
ここで使った `call_count` のような、モックに何が起きたかを知るための属性については、公式ドキュメントの [`Mock` クラスの説明](https://docs.python.org/ja/3/library/unittest.mock.html#the-mock-class) に載っています。
:::

### デコレータとして使う

patcher をデコレータとして使うと、`with` ブロックではなく関数やクラスのスコープで一時的にモックすることができます。
次のコードは先ほどと同じことをデコレータで書いた例です。

```python:tests/test_06.py
from unittest import TestCase
from unittest.mock import patch
from my_class import Event

class EventTest(TestCase):
    @patch('my_class.get_available_seats')
    def test_availability(self, mock_func):
        # Event を作る
        event = Event(id=1)

        # 残席数が 0 の場合は「完売」となること
        mock_func.return_value = 0
        availability = event.check_availability()
        self.assertEqual(availability, '完売')

        # 残席数が 19 の場合は「残り僅か」となること
        mock_func.return_value = 19
        availability = event.check_availability()
        self.assertEqual(availability, '残り僅か')

        # my_class.get_available_seats が2回呼ばれたことを確認する
        self.assertEqual(mock_func.call_count, 2)
```

デコレータで書いた場合は、作られたモックオブジェクトは関数の引数として受け取ることができます。

::: note
複数のデコレータを使う場合の引数の受け取り方については、こちらをご覧ください。

- [pathcer - patch デコレータをネストする (python.org)](https://docs.python.org/ja/3/library/unittest.mock.html#nesting-patch-decorators)
:::

## patcher を使うべき理由

モックを戻す時は、自力で元の関数に戻すのでなく patcher を使うべきです。
理由は、テストが失敗した時に patcher でモックしたものは自動的に元に戻ってくれるからです。

テストが失敗した時 (AssertionError が出た時) は、そこで関数の実行が終わってテストランナーに処理が戻ります。
この時、コンテキストマネジャなら `with` ブロックを出る処理が、デコレータなら関数を出る処理がちゃんと行われるので、モックが元に戻ってから次のテストが実行されるのです。

# その他の基本パターン

ここまで、モックオブジェクトの基本原理についてなるべく分かりやすく説明したつもりです。
この原理を知らなくても、何となくで使えてしまうのもモックの良いところなのですが、知っていれば「こんなこともできるかも」という発想が生まれます。

最後に、現場でよく使う基本パターンについて簡単に列挙します。

- [`side_effect`](https://docs.python.org/ja/3/library/unittest.mock.html#unittest.mock.Mock.side_effect) で処理を置き換える、または意図的に例外を出す
- [`patch.object()`](https://docs.python.org/ja/3/library/unittest.mock.html#patch-object) ですでに `import` しているオブジェクトの属性をモックする
- モックオブジェクトの [`assert_xx` 系の関数](https://docs.python.org/ja/3/library/unittest.mock.html#unittest.mock.Mock.assert_called) でアサーションする
- モックオブジェクトの [`call_xx` 系の属性](https://docs.python.org/ja/3/library/unittest.mock.html#unittest.mock.Mock.assert_called) で呼び出し回数やその時の引数を知る
- その時、[`wraps`](https://docs.python.org/ja/3/library/unittest.mock.html#unittest.mock.Mock.call_args:~:text=wraps%3A%20%E3%81%93%E3%81%AE%E3%83%A2%E3%83%83%E3%82%AF%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%8C%E3%83%A9%E3%83%83%E3%83%97%E3%81%99%E3%82%8B%E3%82%82%E3%81%AE%E3%81%A7%E3%81%99%E3%80%82) で元の関数は呼ばれるようにしておく