【Python】テスト・モックのやり方とその仕組み

# 概要

Pythonでテストといえば、unittestやpytestが有名ですが、unittestやpytestを使わないやり方、unittestとpytestの違いなどを解説していきたいと思います。まず、テストするにあたり、モックのための前提知識として、再宣言について解説し、次に実際にテストの解説をしていきます。

この記事を読むと以下のことが理解できるようになります。

* [再宣言について](#再宣言)
* [ライブラリを用いないテストの使い方](#ライブラリを使わずにテストする)
* [なぜライブラリが必要なのか？](#モックの問題点)
* [unittestの使い方](#unittest)
* [pytestの使い方](#pytest)

初心者の方やPythonのテストの仕組みについて知りたい方におすすめです。

# 再宣言

PythonではGoやJavaなどの言語とは異なり、再宣言が可能です。

```python
a = 1
a = 1 # エラーにならない
```

他の言語では以下のようにコンパイルエラーになります。(以下はGoの例)

```go:goの場合
func main() {
    a := 10
    a := "Hello" // コンパイルエラー
    fmt.Println(a)
}
```

また、関数も同様に再宣言できます。

```python
def hoge():
    return 1

def hoge2():
    return 99
hoge = hoge2
print(hoge()) # 99
```

再宣言ができるということは、実装を置き換えたり、テストでモックすることが可能になります。

# テストのディレクトリ

まず、今回は以下のディレクトリ構想でテストを行います。`app`配下にソースコード、`tests`配下にテストを書いていきます。

```txt
.
├── app 
│   ├── sample1.py
│   └── __init__.py
└── tests  // テストディレクトリ
    ├── __init__.py
    └── test_sample1.py
```

# ライブラリを使わずにテストする

以下のコードの`foo()`をテストするとします。

```python:app/sample1.py
def foo():
    return hoge() + 1

def hoge():
    return 1

foo() # 2
```

`foo()`をテストするには`hoge()`をモックする必要があります。先ほどの再宣言によって`hoge()`をモックしていきます。

```python:tests/test_sample1.py
from app import sample1

def mock_hoge():
    return 99

def test_foo():
    sample1.hoge = mock_hoge
    assert sample1.foo() == 100 # 成功
```

次にクラスの場合を**app/sample2.py**に実装していきます。

```python:app/sample2.py
class Example:
    def foo(self):
        return self.hoge() + 1

    def hoge(self):
        return 1

print(Example().foo())  # 2
```

クラスの場合も関数とほぼ変わりません。(ちなみに、import が `from app import sample2`になると`Example().foo()`が`sample2.Example().foo()`にする必要があります。

```python:tests/test_sample2.py
from app.sample2 import Example

def test_foo():
    Example.hoge = mock_hoge
    assert Example().foo() == 100  # 成功

def mock_hoge(self):
    return 99
```

## モックの問題点

上記のライブラリを使わない方法だと、mock対象の関数などが増えた際に、一つずつ自分で書くと、大変です。
また、モックする前に前もってコピーしておかないと、元の実装戻すことができないという問題が出てきます。
unittestやpytestなどのライブラリを使用することでこれらの問題が解決します。

# unittest

標準ライブラリなので、インストールの必要がありません。

```python:test_sample2.py
import unittest

from app.sample2 import Example


class TestSample2(unittest.TestCase):
    def test_func(self):
        expected = 2
        actual = Example().foo()
        self.assertEqual(expected, actual)


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

## MagicMock

unittest.mockのMagicMockを使ってモックしていきます。

```python:tests/sample2.py
import unittest
from unittest.mock import MagicMock

from app.sample2 import Example


class TestSample2(unittest.TestCase):
    def test_func(self):
        Example.hoge = MagicMock(return_value=99)
        expected = 100
        actual = Example().foo()
        self.assertEqual(expected, actual)
        Example.hoge.assert_called_once()


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

さて、ここで`assert_called_once_with()`という指定した関数で一度だけ呼ばれたことを検証する関数を使っています。MagicMockを使用すると使用することができます

例えば、`hoge()`をモックした`mock_hoge()`の実装が以下のようになっていたら

```python
from app.sample2 import Example


class MockSample2:
    def __init__(self):
        self.count = 0

    def mock_hoge(self):
        self.count += 1
        return 99
    def assert_called_once_with(self):
        if self.count:
            return True
        return False

def test_sample2():
    mock = MockSample2()
    Example.hoge = mock.mock_hoge
    assert 100 == Example().foo()
    assert mock.assert_called_once_with()

```

上記のよう**MockSample2**を実装すると、**MagicMock**の`assert_called_once_with()`と同じ動作をする`assert_called_once_with()`を作成することができます。MagicMockには、`assert_called_once_with()`の他にも色々な関数が実装されております。

| メソッド名 | 説明 |
|:-:|:-:|
| assert_called_with()   |  最後に実行したモックの引数を検証 |
| assert_called_once_with()  |  指定した引数で一度だけ呼ばれたか検証 |
|  assert_any_call() | 指定した引数で一度でも呼ばれたか検証  |
| assert_has_calls()  |  順番通りに呼ばれたか検証 |
| side_effect()  | モックの返り値を設定  |
| side_effect()  | モックの返り値を設定(関数や例外、ランダムな値を返すとき)  |

## patcherでモックを戻す

### withを使ったやり方

withだとスコープを自由に変えることができます。

```python:tests/sample2.py
class TestSample2(unittest.TestCase):
    def test_func(self):
        with patch("app.sample2.Example.hoge") as mock_hoge:
            mock_hoge.return_value = 99
            self.assertEqual(100, Example().foo())

        self.assertEqual(2, Example().foo())


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

### デコレータを使ったやり方

デコレータで書くこともできます。
mockのスコープが1つの関数となります。

```python:tests/sample2.py
class TestSample2(unittest.TestCase):
    @patch("app.sample2.Example.hoge")
    def test_sample2(self, mock_hoge):
        mock_hoge.return_value = 99
        self.assertEqual(100, Example().foo())

    def test_fuck(self):
        self.assertEqual(2, Example().foo())


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

# pytest

pytestです。unittestよりPythonらしい書き方で、よりシンプルに書けます。ただし、インストールが必要となります。

```python:tests/sample2.py
from app.sample2 import Example


def test_func_main(mocker):
    assert 2 == Example().foo()
```

## モック

下記のpytest-mock以外にunittestのmockと同じようにMagicMockが使える。

### pytest-mock

pytest-mockは unittest のmockをフォークしたもの。使用するにはインストールが必要。
フィクスチャを使えて、unittestのmockよりシンプルにかけます。

```python:tests/sample2.py
from app.sample2 import Example


def test_func_main(mocker):
    mocker.patch("app.sample2.Example.hoge", return_value=99)
    assert 100 == Example().foo()
```

### カバレッジや並列実行

pytestでは、カバレッジを出す`pytest-cov`やテストの並列実行を可能にする`python-xdist`といったライブラリがあるので特におすすめです！

:::note warn
pytest-covでカバレッジを出力しているときは、デバッグができないので注意
:::