はじめに
前回はpytestを使用してflaskの単体テストを行いました。しかし、一つのテスト関数の中でflaskのクライアント作成とテストを行っているのでとても見にくいものになりました。今回は、もう少しテストが見やすくなるように色々なpytestの機能を使っていきます。
環境
- python:3.6.5
- flask:1.0.2
- pytest:5.3.5
テストと前処理・後処理の分離
flaskのクライントとテストソースを一つの関数内にまとめていましたが、テスト関数が増えると見にくくなります。また、厳密に言うとクライアント作成はテストではないため、性能テスト時や関数の結果に左右されるのは良くないです。そこでクライアント作成・削除をテストと分離します。
テスト対象のソース
テスト対象のソースは前回のflaskのソースを使用します。
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.fixture
def client():
app.config['TESTING'] = True
test_client = app.test_client()
yield test_client
test_client.delete()
テスト関数
テスト関数を作成して、前処理・後処理のソースでyieldに与えた値を受け取る引数を指定します。この引数は前処理・後処理の関数と同じ名前にする必要があります。その後は普通にテストのソースを記載します。
例では、test_flask_simple()
関数の引数にfixtureで生成したクライアントを受け取る引数を用意してgetを発行してテストしています。
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.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と結果確認に使用しています。
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で調べるにまとめました。