pytestでflaskの単体テストのパラメータをきれいに与える

# はじめに
前回はpytestを使用してflaskの単体テストを行いました。しかし、一つのテスト関数の中でflaskのクライアント作成とテストを行っているのでとても見にくいものになりました。今回は、もう少しテストが見やすくなるように色々なpytestの機能を使っていきます。

# 環境
 - python:3.6.5
 - flask:1.0.2
 - pytest:5.3.5

# テストと前処理・後処理の分離
flaskのクライントとテストソースを一つの関数内にまとめていましたが、テスト関数が増えると見にくくなります。また、厳密に言うとクライアント作成はテストではないため、性能テスト時や関数の結果に左右されるのは良くないです。そこでクライアント作成・削除をテストと分離します。

## テスト対象のソース
テスト対象のソースは前回のflaskのソースを使用します。

``` flask_mod.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def root():
    return "root"

@app.route('/sample/<message>')
def sample(message):
    return 'sample_' + message

```



## 前処理・後処理のソース
前処理と後処理の関数を作成して、その関数を``` @pytest.fixture ```デコレーションで登録します。この関数のyieldにテスト関数が埋め込まれるイメージです。yieldの前に前処理を記載して、yieldの後ろに後処理を記載します。
例では、テスト用のクライアントを生成してyieldに与えています。その後、deleteでクライアントを削除しています。

``` pytest_flask.py
@pytest.fixture
def client():
    app.config['TESTING'] = True
    test_client = app.test_client()
    yield test_client
    test_client.delete()

```

## テスト関数
テスト関数を作成して、前処理・後処理のソースでyieldに与えた値を受け取る引数を指定します。この引数は前処理・後処理の関数と同じ名前にする必要があります。その後は普通にテストのソースを記載します。
例では、``` test_flask_simple() ```関数の引数にfixtureで生成したクライアントを受け取る引数を用意してgetを発行してテストしています。

``` pytest_flask.py
import pytest
from flask_mod import app

@pytest.fixture
def client():
    app.config['TESTING'] = True
    test_client = app.test_client()
    yield test_client
    test_client.delete()


def test_flask_simple(client):
    result = client.get('/')
    assert b'root' == result.data

```

## 実行結果
テスト対象とテスト方法のソースができたため、実行します。

```
PS C:\Users\xxxx\program\python> pytest .\pytest_flask.py
======= test session starts ========  
platform win32 -- Python 3.6.5, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\xxxx\program\python\flask
collected 1 item

pytest_flask.py .      [100%]  

======== 1 passed in 0.21s =========  

```

結果を見ると先ほど作成したpytest_flaskが100%となり正常に終わっています。テストOKであり、fixtureを使用してクライアントの生成と破棄が正常にできました。


# テストソースを複数のパラメータで使い回す。
テストソースを作って別の引数でテストしたいと言うのは往々にしてあると思います。その場合、``` @pytest.fixture() ```デコレーションの``` param ```引数で登録します。

## テストパラメータの定義
前処理・後処理の関数の``` @pytest.fixture() ```デコレータのparamsにテストパラメータをタプルのリスト形式で記載します。それらのパラメータを受け取る引数を関数に用意してyieldでparamを与えます。
例では、テストされるソースは``` sample(message) ```をイメージしているため、タプルの1番目にflaskに与えるパラメータを定義して、2番目には答えを入れています。

``` pytest_flask.py
@pytest.fixture(params=[('message', b'sample_message'),('sample', b'sample_sample')])
def client(request):
    app.config['TESTING'] = True
    test_client = app.test_client()
    yield test_client, request.param
    test_client.delete()
```

## テスト関数
テスト関数の引数に、前処理・後処理の関数で``` yield ```で与えた値がタプルで入っているため、それぞれ必要な要素を抜き出して使用します。
例では、``` client ```引数内の1番目にclient、2番目にparamsで与えたタプルの1つが入っているため、それを抜き出してURLと結果確認に使用しています。

``` pytest_flask.py
import pytest
from flask_mod import app

@pytest.fixture(params=[('message', b'sample_message'),('sample', b'sample_sample')])
def client(request):
    app.config['TESTING'] = True
    test_client = app.test_client()
    yield test_client, request.param
    test_client.delete()

def test_flask_simple(client):
    test_client = client[0]
    test_param = client[1]
    result = test_client.get('/sample/' + test_param[0])
    assert test_param[1] == result.data

```

## 実行結果
テスト対象とテスト方法のソースができたため、実行します。

```
PS Users\xxxx\program\python>  pytest -v .\pytest_flask.py
======= test session starts ========
platform win32 -- Python 3.6.5, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- c:\users\xxxx\appdata\local\programs\python\python36-32\python.exe
cachedir: .pytest_cache
rootdir:  C:\Users\xxxx\program\python\flask
collected 2 items                                                                                                                    

pytest_flask.py::test_flask_simple[client0] PASSED   [ 50%] 
pytest_flask.py::test_flask_simple[client1] PASSED   [100%] 

======== 2 passed in 0.20s ========= 

```

結果を見ると先ほど作成したtest_flask_simple関数が2回PASSEDになっています。これは、fixtureで与えたタプルが2つなので2回テストして両方OKだったということです。

## 片方失敗してみる
ちゃんと値が渡っているか確認するため、片方だけ誤りの値を与えてみます。

```
PS Users\xxxx\program\python>  pytest -v .\pytest_flask.py
======= test session starts ========
platform win32 -- Python 3.6.5, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- c:\users\xxxx\appdata\local\programs\python\python36-32\python.exe
cachedir: .pytest_cache
rootdir:  C:\Users\xxxx\program\python\flask
collected 2 items

pytest_flask.py::test_flask_simple[client0] FAILED                 [ 50%]
pytest_flask.py::test_flask_simple[client1] PASSED                 [100%] 

============= FAILURES ============= 
_______ test_flask_simple[client0] _______ 

client = (<FlaskClient <Flask 'flask_mod'>>, ('message', b'sample_detail'))

    def test_flask_simple(client):
        test_client = client[0]
        test_param = client[1]
        result = test_client.get('/sample/' + test_param[0])
>       assert test_param[1] == result.data
E       AssertionError: assert b'sample_detail' == b'sample_message'
E         At index 7 diff: b'd' != b'm'
E         Full diff:
E         - b'sample_detail'
E         + b'sample_message'

pytest_flask.py:17: AssertionError
============= 1 failed, 1 passed in 0.27s =============
```

片方だけ失敗する値を与えてみると関数の片方がFAILEDになり失敗しました。


# おわりに
ここでまとめたpytestは、ほんの一部の機能になります。この他にもパラメータの組み合わせを自動で作る機能やデータを保存する機能などさらに便利な機能があります。ただし今回もそうですが使用するのに若干の癖があり初見では使いにくいと感じてしまうかもしれません。しかし、便利で使いやすい機能が豊富なので慣れれば慣れるほど早く多彩なテストが作れるのではないかと思います。
単体テストにつきもののカバレッジの確認方法を[pythonのカバレッジをpytest-covで調べる](https://qiita.com/mink0212/items/34b9def61d58ab781714)にまとめました。