pytest使い方まとめ

今まで**「テストコード」**をほとんど書いたことがなかったのですが、pythonのテストコードを書く機会があり、慌てて使い方を学びました。本記事ではpython単体テストのフレームワークであるpytestの使い方を要点を絞ってまとめました。

参考：公式ドキュメント(https://docs.pytest.org/en/latest/contents.html)

---

## pytestとは

pythonでの単体テストを行うためのフレームワーク。同様なものでunittestというものがあるが、pytestの方が人気らしい。
下記がpytestの特徴

- テスト失敗時の情報が詳細
- テスト対象のモジュール、関数を自動で発見
- フィクスチャ機能を利用することで、モックなど、テストの前処理ができる。(後述)
- unittestにも準拠している

##  インストール
`pip install -U pytest`

## 簡単な使用方法

### 単純な単体テスト
基本的に、下記の要領でテストを実行できる。

- テストケースを`assert <条件式>` で記載する
- `pytest`を実行する

ファイル名, メソッド名を`test_*`で記載すると、自動でテストコードを発見してくれる(自分で指定することも可能)


```python:test_sample.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5
```
上記test_sample.pyファイルを作成し、
`pytest`を実行。

すると、下記のようなテスト結果が表示される。
<details><summary>テスト結果</summary><div>

```
========================================== test session starts ===========================================
platform darwin -- Python 3.7.6, pytest-5.4.3, py-1.8.1, pluggy-0.13.1
rootdir: /Users/xin/work/study/pytest
plugins: hypothesis-5.5.4, arraydiff-0.3, remotedata-0.3.2, openfiles-0.4.0, doctestplus-0.5.0, astropy-header-0.1.2
collected 1 item                                                                                         

test_sample.py F                                                                                   [100%]

================================================ FAILURES ================================================
______________________________________________ test_answer _______________________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_sample.py:5: AssertionError
======================================== short test summary info =========================================
FAILED test_sample.py::test_answer - assert 4 == 5
=========================================== 1 failed in 0.18s ============================================
```

</div></details>

### 例外処理のテスト
`pytest.raises`により、例外のテストが可能

```python:error_sample.py
import pytest

def f():
    raise SystemExit(1)

def test_mytest():
    with pytest.raises(SystemExit):
        f()
```

### クラス内の複数のテストの実行
`Test`をprefixとしたクラスを作成することで、内部のテストメソッドを同時にテストできる。

```python:test_class.py
class TestClass:
    def test_one(self):
        x = "this"
        assert "h" in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, "check")
```

## フィクスチャ
### フィクスチャとは
テストの実行環境をセットアップするために用意されたツール。
いくつか便利そうなものをピックアップします。
詳細は下記を参照(https://docs.pytest.org/en/latest/fixture.html)

### tmpdir
`tmpdir`フィクスチャにより、テスト時に一時的に利用できる固有のディレクトリを作成できる。

```python:test_tempdir.py
import os

def test_create_file(tmpdir):
    p = tmpdir.mkdir("sub").join("hello.txt") # 一時ディレクトリの作成
    p.write("content")
    assert p.read() == "content"
    assert len(tmpdir.listdir()) == 1
    assert 0
```

### monkeypatch
`monkeypatch`フィクスチャにより、オブジェクトのモックを作成できる。
例えば下記のようなプロダクトコードがあり、仮に一`get_value()`が容易に実行できない関数だとする。

```python:monkeypatch_product.py
def return_value(): # プロダクトコード
    a = get_value()
    return a

def get_value(): # 容易に実行できない関数
    return 1
```
この時、`monkeypatch.setattr`により`get_value()`を自作のモック関数に差し替えてテストを実行することができる。

```python:monkeypatch_test.py
import monkeypatch_product

# テストコード
def test_return_value(monkeypatch):
    def mock_get_value():
        return 100

    # モック関数に差し替え
    monkeypatch.setattr(monkeypatch_product, "get_value", mock_get_value)
    res = monkeypatch_product.return_value()
    assert res == 100

```

また、`monkeypatch.setenv`で環境変数のモック化も可能。

```python:monkeypatch_env.py
import os

# プロダクトコード
def get_os_user_lower():
    username = os.getenv("USER")

    if username is None:
        raise OSError("USER environment is not set.")

    return username.lower()

# テストコード
def test_get_os_user_lower(monkeypatch):
    monkeypatch.setenv("USER", "TestingUser")
    assert get_os_user_lower() == "testinguser"
```