Flaskアプリケーションのテスト
テストされていないものが壊れています。
この引用の出所は不明であり、完全に正しいわけではありませんが、真実からそう遠くはありません。 テストされていないアプリケーションは既存のコードを改善することを困難にし、テストされていないアプリケーションの開発者はかなり妄想的になる傾向があります。 アプリケーションに自動テストがある場合は、安全に変更を加えて、何かが壊れているかどうかを即座に知ることができます。
Flaskは、WerkzeugテストClient
を公開し、コンテキストローカルを処理することで、アプリケーションをテストする方法を提供します。 その後、お気に入りのテストソリューションでそれを使用できます。
このドキュメントでは、テストの基本フレームワークとして pytest パッケージを使用します。 次のように、pip
を使用してインストールできます。
pip install pytest
アプリケーション
まず、テストするアプリケーションが必要です。 チュートリアルのアプリケーションを使用します。 そのアプリケーションをまだお持ちでない場合は、からソースコードを入手してください :gh: `例 ` 。
テストスケルトン
まず、アプリケーションルートの下にtestsディレクトリを追加します。 次に、テストを保存するPythonファイル(test_flaskr.py
)を作成します。 test_*.py
のようにファイル名をフォーマットすると、pytestによって自動検出されます。
次に、client()
という pytestフィクスチャを作成します。これは、アプリケーションをテスト用に構成し、新しいデータベースを初期化します。
import os
import tempfile
import pytest
from flaskr import flaskr
@pytest.fixture
def client():
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True
client = flaskr.app.test_client()
with flaskr.app.app_context():
flaskr.init_db()
yield client
os.close(db_fd)
os.unlink(flaskr.app.config['DATABASE'])
このクライアントフィクスチャは、個々のテストごとに呼び出されます。 これにより、アプリケーションへのシンプルなインターフェイスが提供され、アプリケーションへのテストリクエストをトリガーできます。 クライアントはまた、私たちのためにクッキーを追跡します。
セットアップ中に、TESTING
構成フラグがアクティブになります。 これにより、リクエスト処理中のエラーキャッチが無効になり、アプリケーションに対してテストリクエストを実行する際のエラーレポートが改善されます。
SQLite3はファイルシステムベースであるため、tempfile
モジュールを使用して一時データベースを簡単に作成して初期化できます。 mkstemp()
関数は、低レベルのファイルハンドルとランダムなファイル名を返します。後者はデータベース名として使用します。 os.close()
関数を使用してファイルを閉じることができるように、 db_fd を保持する必要があります。
テスト後にデータベースを削除するには、フィクスチャはファイルを閉じてファイルシステムから削除します。
ここでテストスイートを実行すると、次の出力が表示されます。
$ pytest
================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 0 items
=========== no tests ran in 0.07 seconds ============
実際のテストは実行されませんでしたが、flaskr
アプリケーションが構文的に有効であることがすでにわかっています。そうでない場合、インポートは例外で終了します。
最初のテスト
次に、アプリケーションの機能のテストを開始します。 アプリケーションのルート(/
)にアクセスした場合、アプリケーションに「これまでのところエントリがありません」と表示されていることを確認しましょう。 これを行うには、次のような新しいテスト関数をtest_flaskr.py
に追加します。
def test_empty_db(client):
"""Start with a blank database."""
rv = client.get('/')
assert b'No entries here so far' in rv.data
テスト関数が test という単語で始まることに注意してください。 これにより、 pytest は、実行するテストとして関数を自動的に識別できます。
client.get
を使用することで、HTTP GET
リクエストを指定されたパスでアプリケーションに送信できます。 戻り値はresponse_class
オブジェクトになります。 これで、data
属性を使用して、アプリケーションからの戻り値(文字列として)を検査できます。 この場合、'No entries here so far'
が出力の一部であることを確認します。
もう一度実行すると、1つの合格テストが表示されます。
$ pytest -v
================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 1 items
tests/test_flaskr.py::test_empty_db PASSED
============= 1 passed in 0.10 seconds ==============
ログインとログアウト
アプリケーションの機能の大部分は管理ユーザーのみが利用できるため、テストクライアントをアプリケーションにログインおよびログアウトする方法が必要です。 これを行うために、必要なフォームデータ(ユーザー名とパスワード)を使用して、ログインページとログアウトページにいくつかのリクエストを送信します。 また、ログインページとログアウトページがリダイレクトされるため、クライアントに follow_redirects を指示します。
次の2つの関数をtest_flaskr.py
ファイルに追加します。
def login(client, username, password):
return client.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(client):
return client.get('/logout', follow_redirects=True)
これで、ログインとログアウトが機能し、無効な資格情報で失敗することを簡単にテストできます。 この新しいテスト関数を追加します。
def test_login_logout(client):
"""Make sure login and logout works."""
rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
assert b'You were logged in' in rv.data
rv = logout(client)
assert b'You were logged out' in rv.data
rv = login(client, flaskr.app.config['USERNAME'] + 'x', flaskr.app.config['PASSWORD'])
assert b'Invalid username' in rv.data
rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'] + 'x')
assert b'Invalid password' in rv.data
メッセージの追加をテストする
また、メッセージの追加が機能することをテストする必要があります。 次のような新しいテスト関数を追加します。
def test_messages(client):
"""Test that messages work."""
login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
rv = client.post('/add', data=dict(
title='<Hello>',
text='<strong>HTML</strong> allowed here'
), follow_redirects=True)
assert b'No entries here so far' not in rv.data
assert b'<Hello>' in rv.data
assert b'<strong>HTML</strong> allowed here' in rv.data
ここでは、HTMLがテキストで許可されているが、タイトルでは許可されていないことを確認します。これは意図された動作です。
これを実行すると、3つの合格テストが得られるはずです。
$ pytest -v
================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 3 items
tests/test_flaskr.py::test_empty_db PASSED
tests/test_flaskr.py::test_login_logout PASSED
tests/test_flaskr.py::test_messages PASSED
============= 3 passed in 0.23 seconds ==============
その他のテストのコツ
上記のようにテストクライアントを使用する以外に、test_request_context()
メソッドをwith
ステートメントと組み合わせて使用して、要求コンテキストを一時的にアクティブ化することもできます。 これにより、ビュー関数のように request 、 g 、および session オブジェクトにアクセスできます。 このアプローチを示す完全な例を次に示します。
import flask
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
コンテキストにバインドされている他のすべてのオブジェクトは、同じ方法で使用できます。
さまざまな構成でアプリケーションをテストする必要があり、それを行うための適切な方法がないように思われる場合は、アプリケーションファクトリに切り替えることを検討してください(アプリケーションファクトリを参照)。
ただし、テスト要求コンテキストを使用している場合、before_request()
およびafter_request()
関数は自動的に呼び出されないことに注意してください。 ただし、teardown_request()
関数は、テスト要求コンテキストがwith
ブロックを離れるときに実際に実行されます。 before_request()
関数も呼び出す場合は、preprocess_request()
を自分で呼び出す必要があります。
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
app.preprocess_request()
...
これは、アプリケーションの設計方法によっては、データベース接続などを開くために必要になる場合があります。
after_request()
関数を呼び出す場合は、process_response()
を呼び出す必要がありますが、応答オブジェクトを渡す必要があります。
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
resp = Response('...')
resp = app.process_response(resp)
...
その時点でテストクライアントの使用を直接開始できるため、これは一般的にあまり役に立ちません。
偽のリソースとコンテキスト
バージョン0.10の新機能。
非常に一般的なパターンは、ユーザー認証情報とデータベース接続をアプリケーションコンテキストまたは flask.g オブジェクトに保存することです。 このための一般的なパターンは、最初の使用時にオブジェクトをそこに配置し、次に分解時にオブジェクトを削除することです。 たとえば、現在のユーザーを取得する次のコードを想像してみてください。
def get_user():
user = getattr(g, 'user', None)
if user is None:
user = fetch_current_user_from_database()
g.user = user
return user
テストの場合、コードを変更せずに、このユーザーを外部からオーバーライドすると便利です。 これは、 flask.appcontext_pushed 信号をフックすることで実現できます。
from contextlib import contextmanager
from flask import appcontext_pushed, g
@contextmanager
def user_set(app, user):
def handler(sender, **kwargs):
g.user = user
with appcontext_pushed.connected_to(handler, app):
yield
そしてそれを使用するには:
from flask import json, jsonify
@app.route('/users/me')
def users_me():
return jsonify(username=g.user.username)
with user_set(app, my_user):
with app.test_client() as c:
resp = c.get('/users/me')
data = json.loads(resp.data)
self.assert_equal(data['username'], my_user.username)
コンテキストを維持する
バージョン0.4の新機能。
定期的なリクエストをトリガーしても、コンテキストをもう少し長く維持して、追加のイントロスペクションが発生するようにすると便利な場合があります。 Flask 0.4では、test_client()
とwith
ブロックを使用することでこれが可能です。
app = flask.Flask(__name__)
with app.test_client() as c:
rv = c.get('/?tequila=42')
assert request.args['tequila'] == '42'
with
ブロックなしでtest_client()
のみを使用すると、リクエストが使用できなくなったため、assert
はエラーで失敗します(実際のリクエストの範囲外で使用しようとしています)。
セッションへのアクセスと変更
バージョン0.8の新機能。
テストクライアントからセッションにアクセスまたは変更すると非常に役立つ場合があります。 一般に、これには2つの方法があります。 セッションで特定のキーが特定の値に設定されていることを確認したいだけの場合は、コンテキストを維持して flask.session にアクセスできます。
with app.test_client() as c:
rv = c.get('/')
assert flask.session['foo'] == 42
ただし、これでは、要求が発生する前にセッションを変更したり、セッションにアクセスしたりすることはできません。 Flask 0.8から、テストクライアントのコンテキストでセッションを開き、それを変更するための適切な呼び出しをシミュレートする、いわゆる「セッショントランザクション」を提供します。 トランザクションの終了時に、セッションが保存され、テストクライアントで使用できるようになります。 これは、使用されるセッションバックエンドとは独立して機能します。
with app.test_client() as c:
with c.session_transaction() as sess:
sess['a_key'] = 'a value'
# once this is reached the session was stored and ready to be used by the client
c.get(...)
この場合、 Flask.session プロキシの代わりにsess
オブジェクトを使用する必要があることに注意してください。 ただし、オブジェクト自体は同じインターフェイスを提供します。
JSONAPIのテスト
バージョン1.0の新機能。
FlaskはJSONを強力にサポートしており、JSONAPIを構築するための一般的な選択肢です。 JSONデータを使用してリクエストを作成し、応答でJSONデータを調べると非常に便利です。
from flask import request, jsonify
@app.route('/api/auth')
def auth():
json_data = request.get_json()
email = json_data['email']
password = json_data['password']
return jsonify(token=generate_token(email, password))
with app.test_client() as c:
rv = c.post('/api/auth', json={
'username': 'flask', 'password': 'secret'
})
json_data = rv.get_json()
assert verify_token(email, json_data['token'])
テストクライアントメソッドでjson
引数を渡すと、リクエストデータがJSONシリアル化されたオブジェクトに設定され、コンテンツタイプがapplication/json
に設定されます。 get_json
を使用して、リクエストまたはレスポンスからJSONデータを取得できます。
CLIコマンドのテスト
Clickには、CLIコマンドをテストするためのユーティリティが付属しています。 CliRunner
はコマンドを分離して実行し、Result
オブジェクトに出力をキャプチャします。
Flaskはtest_cli_runner()
を提供して、FlaskアプリをCLIに自動的に渡すFlaskCliRunner
を作成します。 invoke()
メソッドを使用して、コマンドラインから呼び出すのと同じ方法でコマンドを呼び出します。
import click
@app.cli.command('hello')
@click.option('--name', default='World')
def hello_command(name)
click.echo(f'Hello, {name}!')
def test_hello():
runner = app.test_cli_runner()
# invoke the command directly
result = runner.invoke(hello_command, ['--name', 'Flask'])
assert 'Hello, Flask' in result.output
# or by name
result = runner.invoke(args=['hello'])
assert 'World' in result.output
上記の例では、コマンドを名前で呼び出すと、コマンドがアプリに正しく登録されていることが確認されるので便利です。
コマンドを実行せずに、コマンドがパラメーターを解析する方法をテストする場合は、make_context()
メソッドを使用します。 これは、複雑な検証ルールとカスタムタイプをテストする場合に役立ちます。
def upper(ctx, param, value):
if value is not None:
return value.upper()
@app.cli.command('hello')
@click.option('--name', default='World', callback=upper)
def hello_command(name)
click.echo(f'Hello, {name}!')
def test_hello_params():
context = hello_command.make_context('hello', ['--name', 'flask'])
assert context.params['name'] == 'FLASK'