Flaskアプリケーションのテスト Testing Flask Applications

テストされていないものは壊れる **Something that is untested is broken.**

この言葉の源は不明であり、そして完全に正しいわけではないですが、それでも真実からかけ離れているわけではありません。テストされていないアプリケーションは、存在するコードを改善することが難しく、テストされていないアプリケーションの開発者は極端な偏執症になりがちです(訳注: 変更を極端に避けようとする姿勢を指していると思います)。もしアプリケーションの自動テストがあれば、安全に変更することができ、もしも何かしら壊れた場合には即座に分かるようになります。 The origin of this quote is unknown and while it is not entirely correct, it is also not far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to become pretty paranoid. If an application has automated tests, you can safely make changes and instantly know if anything breaks.

Flaskは、Werkzeugのテスト用Clientを使えるようにすることによって、さらにcontext localsを操作することによって、自分のアプリケーションをテストする方法を提供します。そして、あなたはそれらを自分の好きなテスト用のソリューションと一緒に使うことができます。 Flask provides a way to test your application by exposing the Werkzeug test :class:`~werkzeug.test.Client` and handling the context locals for you. You can then use that with your favourite testing solution.

このドキュメントでは、テスト用の基盤となるフレームワークとしてpytestパッケージを使用していきます。以下のように、pipを使ってそれをインストールできます: In this documentation we will use the `pytest`_ package as the base framework for our tests. You can install it with ``pip``, like so::

$ pip install pytest

アプリケーション The Application

第一に、アプリケーションをテストする必要があります;この文書では、チュートリアルのアプリケーションを使用していきます。もしもそのアプリケーションをまだ持っていない場合、チュートリアルの例からソースコードを取得してください。 First, we need an application to test; we will use the application from the :doc:`tutorial/index`. If you don't have that application yet, get the source code from :gh:`the examples <examples/tutorial>`.

So that we can import the module flaskr correctly, we need to run pip install -e . in the folder tutorial.

テストの骨組(The Testing Skeleton) The Testing Skeleton

ここでは、アプリケーションのルートの下にtestsディレクトリを加えることから始めます。それから、テストを格納するPythonファイル(test_flaskr.py)を作成します。ファイル名をtest_*.pyのような形式にしたときは、そのファイルはpytestによって自動的に見つけ出されます。 We begin by adding a tests directory under the application root. Then create a Python file to store our tests (:file:`test_flaskr.py`). When we format the filename like ``test_*.py``, it will be auto-discoverable by pytest.

次に、テスト用にアプリケーションを設定し、新しいデータベースを初期化する、client()と呼ばれる(訳注: 以下の例で「client」という名前で定義されていることを指しています)pytestのfixtureを作成します: Next, we create a `pytest fixture`_ called :func:`client` that configures the application for testing and initializes a new database::

import os
import tempfile

import pytest

from flaskr import create_app


@pytest.fixture
def client():
    db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
    flaskr.app.config['TESTING'] = True

    with flaskr.app.test_client() as client:
        with flaskr.app.app_context():
            flaskr.init_db()
        yield client

    os.close(db_fd)
    os.unlink(flaskr.app.config['DATABASE'])

このclientのfixtureは、個別のテストそれぞれから呼び出されます。そのfixtureは、アプリケーションへのテスト用のリクエストを引き起こせるようにする、アプリケーションへのシンプルなインタフェースを供給します。そのclientは、cookieの経過も追跡します。 This client fixture will be called by each individual test. It gives us a simple interface to the application, where we can trigger test requests to the application. The client will also keep track of cookies for us.

setupの間、TESTING設定フラグ(訳注: flask.ConfigオブジェクトでTESTINGキーで設定される情報で、直前のコード例でapp.config['TESTING']として操作している箇所)が有効化されます。これは、リクエストの処理中にエラーを捕捉しないようにすることで、アプリケーションに対するテスト用のリクエストを実施するときに、より良いエラーのレポートを得られるようにします。 During setup, the ``TESTING`` config flag is activated. What this does is disable error catching during request handling, so that you get better error reports when performing test requests against the application.

SQLite3はファイルシステムをベースにしているため(訳注: データをローカルのファイルとして保存し、データベースへのアクセスにネットワーク越しのサーバへのアクセスなどを必要としないことが、ここではポイントになります)、tempfileモジュールを使用して、容易に一時的なデータベースを作成して初期化できます。mkstemp()関数は2つのことを行います: 低レベルでのファイル処理用ハンドルと、後でデータベース名として使用できるランダムなファイル名を返します。db_fd(訳注: 直前のコード例にあるdb_fd変数のことで、mkstempで作成したファイル処理用ハンドルを格納します)だけ保持していれば、os.close()関数を使ってそのファイルを閉じることが可能です。 Because SQLite3 is filesystem-based, we can easily use the :mod:`tempfile` module to create a temporary database and initialize it. The :func:`~tempfile.mkstemp` function does two things for us: it returns a low-level file handle and a random file name, the latter we use as database name. We just have to keep the `db_fd` around so that we can use the :func:`os.close` function to close the file.

テストの後にテスト用のデータベースを消去するために、fixtureはそのファイルを閉じて、ファイルシステムからそのファイルを削除します。 To delete the database after the test, the fixture closes the file and removes it from the filesystem.

この時点でテスト一式を実行した場合、以下のような出力を確認できるはずです: If we now run the test suite, we should see the following output::

$ pytest

================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 0 items

=========== no tests ran in 0.07 seconds ============

たとえ実際のテストは何も実行していないとしても、この時点でflaskrアプリケーションは文法的に正当であることが分かり、そうでなければ(pytestの実行のとき)importが例外により異常終了します。 Even though it did not run any actual tests, we already know that our ``flaskr`` application is syntactically valid, otherwise the import would have died with an exception.

最初のテスト The First Test

ここまできたら、アプリケーションの機能のテストを始めるときです。もしもアプリケーションのルート(/)へアクセスした場合に「No entries here so far」をアプリケーションが表示するかチェックしましょう。これを行うために、以下のように、test_flaskr.pyへ新しいテスト関数を追加します: Now it's time to start testing the functionality of the application. Let's check that the application shows "No entries here so far" if we access the root of the application (``/``). To do this, we add a new test function to :file:`test_flaskr.py`, like this::

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がその関数を実行すべきテストだと自動的に特定できるようにします。 Notice that our test functions begin with the word `test`; this allows `pytest`_ to automatically identify the function as a test to run.

client.getを使用することで、(引数で)与えられたパスを伴ったHTTPのGETリクエストをアプリケーションに送ることができます。その戻り値はresponse_classオブジェクトになります。それから、data属性を使ってアプリケーションからの戻り値を(文字列として)調べることができます。このケースでは、「No entries here so far」が出力の一部であることを確認します。 By using ``client.get`` we can send an HTTP ``GET`` request to the application with the given path. The return value will be a :class:`~flask.Flask.response_class` object. We can now use the :attr:`~werkzeug.wrappers.Response.data` attribute to inspect the return value (as string) from the application. In this case, we ensure that ``'No entries here so far'`` is part of the output.

(pytestを)再実行すると、1つテストをパスしたことを確認できるはずです: Run it again and you should see one passing test::

$ 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 ==============

ログインとログアウト Logging In and Out

今回のアプリケーションでの主要な機能は、管理者ユーザだけが利用可能になっているため、テスト用のclientがアプリケーションに対してログインおよびログアウトする方法が必要になります。これを行うために、必要なformデータ(ユーザ名とパスワード)を使ってログインおよびログアウトのページへのリクエストを起こします。さらにログインおよびログアウトのページは転送(redirect)されるものなので、clientに対してfollow_redirectsを指示します(訳注: 以下のコード例のように、client.postおよびclient.getにfollow_redirectsのキーワード引数を渡すことを指しています)。 The majority of the functionality of our application is only available for the administrative user, so we need a way to log our test client in and out of the application. To do this, we fire some requests to the login and logout pages with the required form data (username and password). And because the login and logout pages redirect, we tell the client to `follow_redirects`.

以下の2つの関数をtest_flaskr.pyファイルへ追加します: Add the following two functions to your :file:`test_flaskr.py` file::

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)

この時点で、ログインとログアウトが機能し、不正な認証情報(credentials)では(ログインに)失敗することを容易にテストできます。以下の新しい関数を(test_flaskr.pyに)追加します: Now we can easily test that logging in and out works and that it fails with invalid credentials. Add this new test function::

def test_login_logout(client):
    """Make sure login and logout works."""

    username = flaskr.app.config["USERNAME"]
    password = flaskr.app.config["PASSWORD"]

    rv = login(client, username, 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, f"{username}x", password)
    assert b'Invalid username' in rv.data

    rv = login(client, username, f'{password}x')
    assert b'Invalid password' in rv.data

メッセージ追加のテスト Test Adding Messages

メッセージ追加が機能することもテストするべきです。以下のように新しい関数を追加します: We should also test that adding messages works. Add a new test function like this::

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'&lt;Hello&gt;' in rv.data
    assert b'<strong>HTML</strong> allowed here' in rv.data

ここでは、HTMLがテキストでは許されていながら、タイトルでは許されていないという、意図している振舞をチェックします。 Here we check that HTML is allowed in the text but not in the title, which is the intended behavior.

これを(pytestで)実行すると、3つのテストがパスすることを示すはずです: Running that should now give us three passing tests::

$ 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 ==============

その他のテスト用のしかけ(Other Tesing Tricks) Other Testing Tricks

ここまでに示したテスト用clientを使う他に、request contextを一時的に有効にするには、with文と組み合わせて使用できるtest_request_context()メソッドもあります。これを使うとrequest, g, sessionオブジェクトへview関数のようにアクセスできるようになります。以下は、このアプローチを実演(demonstrates)する一通り不足のない例になります: Besides using the test client as shown above, there is also the :meth:`~flask.Flask.test_request_context` method that can be used in combination with the ``with`` statement to activate a request context temporarily. With this you can access the :class:`~flask.request`, :class:`~flask.g` and :class:`~flask.session` objects like in view functions. Here is a full example that demonstrates this approach::

import flask

app = flask.Flask(__name__)

with app.test_request_context('/?name=Peter'):
    assert flask.request.path == '/'
    assert flask.request.args['name'] == 'Peter'

contextに結び付けられた(bound)その他の全てのオブジェクトを同じやり方で使用できます。 All the other objects that are context bound can be used in the same way.

もしも自分のアプリケーションを異なる設定でテストしたいにもかかわらず、相応しいやり方が見つからない場合、application factory(アプリケーション製造工場(Application Factories)を確認してください)へ切り替えることを検討してください。 If you want to test your application with different configurations and there does not seem to be a good way to do that, consider switching to application factories (see :doc:`patterns/appfactories`).

しかしながら、もしもテスト用のrequest contextを使用している場合は、before_request()after_request()関数が自動的には呼び出されないことに注意してください。しかしながら、テスト用のrequest contextがwithブロックを去るときに、teardown_request()関数は実際には実行されます。もしもbefore_request()関数も同様に呼び出されるようにしたい場合は、自分でpreprocess_request()を呼び出す必要があります: Note however that if you are using a test request context, the :meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` functions are not called automatically. However :meth:`~flask.Flask.teardown_request` functions are indeed executed when the test request context leaves the ``with`` block. If you do want the :meth:`~flask.Flask.before_request` functions to be called as well, you need to call :meth:`~flask.Flask.preprocess_request` yourself::

app = flask.Flask(__name__)

with app.test_request_context('/?name=Peter'):
    app.preprocess_request()
    ...

これは、自分のアプリケーションの設計によっては、データベース接続または似たような何かを開くために必要になる可能性があります。 This can be necessary to open database connections or something similar depending on how your application was designed.

もしもafter_request()関数を呼び出したい場合は、呼び出すときにresponseオブジェクトを渡す必要があるprocess_response()を経由する必要があります: If you want to call the :meth:`~flask.Flask.after_request` functions you need to call into :meth:`~flask.Flask.process_response` which however requires that you pass it a response object::

app = flask.Flask(__name__)

with app.test_request_context('/?name=Peter'):
    resp = Response('...')
    resp = app.process_response(resp)
    ...

その時点ではテスト用clientを使用して直接開始できるため、これは概してあまり便利ではありません。 This in general is less useful because at that point you can directly start using the test client.

リソースおよびコンテキストの模倣(Faking Resources and Context) Faking Resources and Context

Changelog

バージョン 0.10 で追加.

ユーザ認証情報とデータベース接続(database connections)をapplication contextまたはflask.gオブジェクトへ格納することは、よくあるパターンになります。これに対する一般的なパターンは、オブジェクトを最初に使用するときにそれ(application contextまたはflask.gオブジェクト)に置き、(リクエストやapplication contextなどを)取り壊すときにそれ(認証情報など)を削除することです。例えば、そのときのユーザを取得するための以下のコードを想像してください: A very common pattern is to store user authorization information and database connections on the application context or the :attr:`flask.g` object. The general pattern for this is to put the object on there on first usage and then to remove it on a teardown. Imagine for instance this code to get the current user::

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シグナルをフックすることで達成可能です: For a test it would be nice to override this user from the outside without having to change some code. This can be accomplished with hooking the :data:`flask.appcontext_pushed` signal::

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

それから、(以下のように)それ(appcontext_pushedをフックする処理)を使用します: And then to use it::

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)
        assert data['username'] == my_user.username

コンテキストの保持(Keeping the Context Around) Keeping the Context Around

Changelog

バージョン 0.4 で追加.

ときには通常のリクエストを引き起こしながら、より多く調査できるようにするため(so that additional introspection can happen)に、少し長い間コンテキストを保持できると役立ちます。Flask 0.4では、withブロックと一緒にtest_client()を使うことで可能です: Sometimes it is helpful to trigger a regular request but still keep the context around for a little longer so that additional introspection can happen. With Flask 0.4 this is possible by using the :meth:`~flask.Flask.test_client` with a ``with`` block::

app = flask.Flask(__name__)

with app.test_client() as c:
    rv = c.get('/?tequila=42')
    assert request.args['tequila'] == '42'

もしもwithブロックを伴わずにtest_client()だけを使用した場合、(実際のリクエストの外側で使用しようとしているため)もはやrequestが利用できないためにassertがエラーを伴って失敗するでしょう。 If you were to use just the :meth:`~flask.Flask.test_client` without the ``with`` block, the ``assert`` would fail with an error because `request` is no longer available (because you are trying to use it outside of the actual request).

セッションに対するアクセスと変更 Accessing and Modifying Sessions

Changelog

バージョン 0.8 で追加.

ときには、テスト用clientからセッションに対してアクセスまたは変更すると非常に役立つことがあります。これについては、概して2つのやり方があります。もしもある値が設定たあるキーをセッションが持っているか確認したいだけである場合は、コンテキストを保持してflask.sessionにアクセスするだけで可能です: Sometimes it can be very helpful to access or modify the sessions from the test client. Generally there are two ways for this. If you just want to ensure that a session has certain keys set to certain values you can just keep the context around and access :data:`flask.session`::

with app.test_client() as c:
    rv = c.get('/')
    assert flask.session['foo'] == 42

これは、しかしながら、セッションの変更やリクエストが引き起こされる前のセッションへのアクセスもできるようにはしません。Flask 0.8からは、テスト用clientのコンテキストの中でセッションを開き、そしてそのセッションを変更する、適切な呼び出しを模倣する「セッショントランザクション(session transaction)」と呼ばれるものを提供しています。これは使用されているセッションの背後の仕組み(session backend)に関係なく機能します: This however does not make it possible to also modify the session or to access the session before a request was fired. Starting with Flask 0.8 we provide a so called “session transaction” which simulates the appropriate calls to open a session in the context of the test client and to modify it. At the end of the transaction the session is stored and ready to be used by the test client. This works independently of the session backend used::

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オブジェクトを使う必要があることに注意してください。しかしながら、sessオブジェクト自体はflask.sessionと同じインタフェースを提供します。 Note that in this case you have to use the ``sess`` object instead of the :data:`flask.session` proxy. The object however itself will provide the same interface.

JSON APIのテスト(Testing JSON APIs) Testing JSON APIs

Changelog

バージョン 1.0 で追加.

FlaskにはJSONへの素晴しいサポートがあり、JSONのAPIを構築するための選択肢として人気があります。JSONデータを使ったリクエストを作成して、レスポンスの中でJSONデータを調べるには非常に好都合です: Flask has great support for JSON, and is a popular choice for building JSON APIs. Making requests with JSON data and examining JSON data in responses is very convenient::

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={
        'email': 'flask@example.com', 'password': 'secret'
    })
    json_data = rv.get_json()
    assert verify_token(email, json_data['token'])

json引数をテスト用clientのメソッドで渡すと、リクエストのデータをJSONでシリアライズされたオブジェクト(JSON-serialized object)に設定し、(リクエストの)content typeをapplication/jsonに設定します。get_jsonを使って、リクエストまたはレスポンスからJSONのデータを取得できます。 Passing the ``json`` argument in the test client methods sets the request data to the JSON-serialized object and sets the content type to ``application/json``. You can get the JSON data from the request or response with ``get_json``.

CLIコマンドのテスト(Testing CLI Commands) Testing CLI Commands

Click(訳注: Flaskインストール時に自動的にインストールされるCLI用のツール)は自分のCLIコマンドをテストするためのユーティリティが一緒になっています。CliRunnerはコマンドを別々に分けながら実行して出力をResultオブジェクトの中に捉えます。 Click comes with `utilities for testing`_ your CLI commands. A :class:`~click.testing.CliRunner` runs commands in isolation and captures the output in a :class:`~click.testing.Result` object.

Flaskは、FlaskのappをCLIへ自動的に渡すFlaskCliRunnerを作成するためにtest_cli_runner()を提供しています。コマンドラインから呼ばれたときと同じようにコマンドを呼び出すために、invoke()メソッドを使用します。: Flask provides :meth:`~flask.Flask.test_cli_runner` to create a :class:`~flask.testing.FlaskCliRunner` that passes the Flask app to the CLI automatically. Use its :meth:`~flask.testing.FlaskCliRunner.invoke` method to call commands in the same way they would be called from the command line. ::

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

上の例では、コマンドがappに適切に登録されたことを検証するため、名前によってコマンドを起動するのが便利です。 In the example above, invoking the command by name is useful because it verifies that the command was correctly registered with the app.

もしも自分のコマンドがどのようにパラメータを解析するか、コマンドを実行せずにテストしたい場合は、make_context()メソッドを使用します。これは複雑な検証ルールや独自タイプ(のパラメータ)をテストするために役立ちます。: If you want to test how your command parses parameters, without running the command, use its :meth:`~click.BaseCommand.make_context` method. This is useful for testing complex validation rules and custom types. ::

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'