pytest で単体テストの方法まとめ

# 概要

Python には [pytest](https://docs.pytest.org/en/stable/) という単体テストを書く機能があり、便利なのですが、他の言語と若干仕様が異なるので、よく使う機能を備忘録としてまとめておきます。

# テストの書き方・実行方法

`CreateXxx` というクラスが `print_aaa` というメソッドを持っている場合のテストケース。

```py:tests/test_create_xxx.py
import pytest
from xxx import CreateXxx, XxxError

class TestCreateXxx:
  # 通常の評価
  def test__can_print_aaa(self):
    xxx = CreateXxx()
    assert xxx.print_aaa(111) == 'aaa'

  # エラーがでることを評価
  def test__can_raise_error(self):
    xxx = CreateXxx()
    with pytest.raises(XxxError):
      xxx.print_aaa(222)
```

このようなファイルを用意し、

```sh:sh実行
$ pytest tests/test_create_xxx.py
```

と実行することで実行。


# いわゆる setup 的なもの

```py:tests/test_create_xxx.py
  @pytest.fixture
  def init_xxx(self):
     self.xxx = CreateXxx()

  def test__can_print_aaa(self, init_xxx):
    assert self.xxx.print_aaa() == 'aaa'
```

- fixture という機能を使って setup を実現します。
- 引数に fixture として定義したメソッド名を書くことでテスト開始直前に実行されることになります。


# いわゆる teardown 的なもの

```py:tests/test_create_xxx.py
  @pytest.fixture
  def init_xxx(self):
     self.xxx = CreateXxx()
     yield
     # ここから teardown
     self.xxx.close()

  def test__can_print_aaa(self, init_xxx):
    assert self.xxx.print_aaa() == 'aaa'
```

- fixture の中で `yield` を呼ぶと、その段階でいったん実行が中断され、テストメソッド本体が実行され、その後、`yield` 以降の処理が実行されることになります。つまり teardown 相当の処理となります。
- 変数をローカルに閉じ込められるので可読性を高められます。
- 戻り値を戻したいときは `yield` の後ろに戻り値を書きます。
- なお、`yield` は１回しか使えません。複数書くと怒られます。


# 戻り値を返す、いわゆる fixture

```py:tests/test_create_xxx.py
  @pytest.fixture
  def xxx(self):
     return CreateXxx()

  def test__can_print_aaa(self, xxx):
    assert xxx.print_aaa() == 'aaa'
```


# 戻り値を複数返す fixture

```py:tests/test_create_xxx.py
  @pytest.fixture
  def init_xxx(self):
     return CreateXxx(), 'aaa'

  def test__can_print_aaa(self, init_xxx):
    xxx, expected = init_xxx
    assert xxx.print_aaa() == expected
```


# データジェネレート

同じテストケースを引数違いで何度も実行したい場合

### @pytest.mark.parametrize を使う方法 (テストケースごとに定義)

シンプル。fixture ではないので、共有はできないし、複雑な処理もできないが、とにかくシンプルにかける。

```py:tests/test_create_xxx.py
  @pytest.mark.parametrize('val, expected', [
    ('aa', 'bb'),
    ('xx', 'yy'),
  ])
  def test__can_print_aaa(self, val, expected):
    assert xxx.print_aaa(val) == expected
```

#### parametrize のテストケースタイトルが文字化けしないようにする方法

##### 全角文字がエスケープされないように

parametrize のテストケースタイトルは扱う値の組み合わせで生成されるのだが、値が全角文字だと `\u306f\u～` のようにエスケープされてどのケースなのか判別できなくなる。
`pytest.ini` というファイルをルートディレクトリに置き、下記を設定することでエスケープされないようにできる：

```
[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
```

##### オブジェクトの内容が表示されるように

parametrize のテストケースタイトルは扱う値の組み合わせで生成されるのだが、デフォルトだとオブジェクトは `hoge_obj1` のようにオブジェクト名＋連番でタイトルに採用されてしまい、内容が全く分からない。
ids を指定することでカスタマイズが可能：

```py:方法1) ids=strを使う方法
@pytest.mark.parametrize('aa, bb', [
  (HogeObj(), FugaObj()),
  ...
], ids=str)
# これですべての値は str() されることになる。str の代わりに repr を指定しても良い
```

```py:方法2) idsで自作関数を指定することも可能
def my_ids(v):
  if type(v) is HogeObj:
    return f'{v.aaa}'
  if type(v) is FugaObj:
    return f'{v.bbb}'
  return v

@pytest.mark.parametrize('aa, bb', [
  (HogeObj(), FugaObj()),
  ...
], ids=my_ids)
```


### fixture を使う方法 (複数のテストケースで共有可能)

```py:tests/test_create_xxx.py
  @pytest.fixture(params=[
    ('aa', 'bb'),
    ('xx', 'yy'),
  ])
  def case__printable(self, request) -> Tuple[str, str]:
    return request.param

  def test__can_print_aaa(self, case__printable):
    val, expected = case__printable
    assert xxx.print_aaa(val) == expected
```


# ファイルをまたいで共通化したい場合: conftest.py

`conftest.py` に書いたものはファイル間で共有される。

```py:tests/conftest.py
@pytest.fixture
def xxx():
   return CreateXxx()
```

```py:tests/test_create_xxx.py
  def test__can_print_aaa(self, xxx):
    assert xxx.print_aaa() == expected
```


# fixture の実行タイミングを明示する

デフォルトは（引数で fixture 名を指定している）テストケースメソッドの開始直前。
これを変更できる。下記のように明示する。

```py
  @pytest.fixture(scope='class')
```

|スコープ名|実行されるタイミング
|---|---
|function    |テストケースごとに1回実行（デフォルト）
|class       |テストクラス全体で1回実行
|module      |テストファイル全体で1回実行
|session     |テスト全体で1回だけ実行


# モック

たとえば、下記のような場合にモックしたくなる。

- AWSなど外部と繋ぐ部分（単体Testでは繋げたくない）
- 現在時、ランダム値を使う部分（毎回変わるのでテストしづらい）
- 外部の責務(=別の単体テストですでにテストしている)部分（テストのメンテで別の責務に引きずられたくない）

## import される関数の中身を一時的に差し替える

- テストケース終了時に元に戻る。


### mocker.patch.object で何もしないようにする

```py:tests/test_create_xxx.py
from pytest_mock import MockFixture
import hoge

class TestCreateXxx:
  def test__can_print_aaa(self, mocker: MockFixture, xxx):
    mocker.patch.object(hoge, 'fuga')  # hoge の fuga という関数を何もしないモックに差し替える
    assert xxx.print_aaa() == 'aaa'
```


### mocker.patch.object で戻り値を定義する

```py:tests/test_create_xxx.py
from pytest_mock import MockFixture
import hoge

class TestCreateXxx:
  def test__can_print_aaa(self, mocker: MockFixture, xxx):
    mocker.patch.object(hoge, 'get_fuga', return_value='fuga')
    assert xxx.print_aaa() == 'aaa'
```


### mocker.patch.object で例外が発生するようにする

```py:tests/test_create_xxx.py
from pytest_mock import MockFixture
import hoge

class TestCreateXxx:
  def test__can_print_aaa(self, mocker: MockFixture, xxx):
    mocker.patch.object(hoge, 'get_fuga', side_effect=hoge.PiyoError('test'))
    with pytest.raises(hoge.PiyoError):
      xxx.print_aaa()
```


### mocker.patch.object で関数に渡された引数をチェックする

```py:tests/test_create_xxx.py
from unittest.mock import call
import hoge

class TestCreateXxx:
  def test__can_print_aaa(self, mocker: MockFixture, xxx):
    m_get_fuga = mocker.patch.object(hoge, 'get_fuga', return_value='fuga')
    xxx.print_aaa()
    assert m_get_fuga.assert_has_calls([
        call(1, 'a'), # 初回呼び出しの第１引数が 1, 第２引数が 'a'
    ])
```


### mocker.patch.object で呼び出しごとに戻り値を変える

```py:tests/test_create_xxx.py
from unittest.mock import call
import hoge

class TestCreateXxx:
  def test__can_print_aaa(self, mocker: MockFixture, xxx):
    # side_effect で配列を渡すと呼び出しごとに順に返すようになる
    m_get_fuga = mocker.patch.object(hoge, 'get_fuga', side_effect=[
        'fuga1',
        'fuga2',
    )
    xxx.print_aaa()  # fuga1 が使われる
    xxx.print_aaa()  # fuga2 が使われる
```


### mocker.patch.object でクラスをモック(Newした際にモックが返るように)

```py:tests/test_create_xxx.py
from unittest.mock import call
import hoge

class TestCreateXxx:
  def test__can_print_aaa(self, mocker: MockFixture):
    # 注意: return_value 必須
    mocker.patch.object(hoge.Hoge, '__init__', return_value=None)
    # 注意: 一見、static メソッドを差し替えてる風に見えるがちゃんと動く
    mocker.patch.object(hoge.Hoge, 'get_fuga', return_value='fuga')

    xxx.print_aaa()
    assert xxx.print_aaa() == 'aaa'
```



## 環境変数を一時的に差し替える

- テストケース終了時に元に戻る。

### monkeypatch.setenv で差し替える

```py:tests/test_create_xxx.py
import hoge

class TestCreateXxx:
  def test__can_print_aaa(self, monkeypatch, xxx):
    monkeypatch.setenv('HOGE', '1234')
    assert xxx.print_aaa(111) == 'aaa'
```


## static なプロパティを一時的に差し替える

- テストケース終了時に元に戻る。

### monkeypatch.setattr で差し替える

```py:tests/test_create_xxx.py
import hoge
import MyConfig

class TestCreateXxx:
  def test__can_print_aaa(self, monkeypatch, xxx):
    monkeypatch.setattr(MyConfig, 'abc', 123)
```


以上！
随時、更新していきます。