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

Flaskはアプリケーションをテストするユーティリティを提供します。このドキュメントはテストの中でアプリケーションの異なる部分で作業するための技法を扱います。 Flask provides utilities for testing an application. This documentation goes over techniques for working with different parts of the application in tests.

ここではテストを準備し走らせるためにpytestフレームワークを使います。 We will use the `pytest`_ framework to set up and run our tests.

$ pip install pytest

チュートリアルはサンプルのブログのアプリケーションであるFlaskrに対して100%の網羅率のテストをどのように書くかを扱います。アプリケーション用の特定のテストの詳細な説明は、テストのチュートリアルを見てください。 The :doc:`tutorial </tutorial/index>` goes over how to write tests for 100% coverage of the sample Flaskr blog application. See :doc:`the tutorial on tests </tutorial/tests>` for a detailed explanation of specific tests for an application.

テストの特定 Identifying Tests

テストは典型的にはtestsフォルダに置かれます。テストはtests_から始まる関数で、test_から始まるPythonモジュール中にあります。さらにテストはTestで始まるクラスの中にグループ化できます。 Tests are typically located in the ``tests`` folder. Tests are functions that start with ``test_``, in Python modules that start with ``test_``. Tests can also be further grouped in classes that start with ``Test``.

何をテストするか知るのは難しいことがあります。大概は、自分で書いたコードをテストしようと試み、使用するライブラリのコードは既にテストされているため試みません。複雑な振る舞いは、個別にテストするために分かれた関数として取り出すことを試みます。 It can be difficult to know what to test. Generally, try to test the code that you write, not the code of libraries that you use, since they are already tested. Try to extract complex behaviors as separate functions to test individually.

据え付け品(Fixtures) Fixtures

Pytestのfixtureはテストで再利用できるコードの断片を書けるようにします。シンプルなfixtureは値を返しますが、fixtureは準備(setup)をし、値を生成(yield)し、それから取り壊す(teardown)こともできます。アプリケーション、テストのクライアント、CLIの実行プログラム用のfixtureが、以下に示されており、それらはtests/conftest.pyに置くことができます。 Pytest *fixtures* allow writing pieces of code that are reusable across tests. A simple fixture returns a value, but a fixture can also do setup, yield a value, then do teardown. Fixtures for the application, test client, and CLI runner are shown below, they can be placed in ``tests/conftest.py``.

もしアプリケーションの製造工場(factory)を使っている場合、appのインスタンスを作成し設定するためのappのfixture(訳注: 以下の例のapp関数のこと)を定義します。データベースの作成と消去のように、他のリソースの準備(set up)と取り壊し(tear down)のために、yieldの前後にコードを加えることができます。 If you're using an :doc:`application factory </patterns/appfactories>`, define an ``app`` fixture to create and configure an app instance. You can add code before and after the ``yield`` to set up and tear down other resources, such as creating and clearing a database.

もしfactoryを使っていない場合、あなたは既にimportして直接設定できるappオブジェクトを持っているでしょう。それでも、リソースのset upおよびtear downのためにappのfixtureを使うことができます。 If you're not using a factory, you already have an app object you can import and configure directly. You can still use an ``app`` fixture to set up and tear down resources.

import pytest
from my_project import create_app

@pytest.fixture()
def app():
    app = create_app()
    app.config.update({
        "TESTING": True,
    })

    # other setup can go here

    yield app

    # clean up / reset resources here


@pytest.fixture()
def client(app):
    return app.test_client()


@pytest.fixture()
def runner(app):
    return app.test_cli_runner()

テスト用クライアントを使ったリクエストの送信 Sending Requests with the Test Client

テスト用クライアントは実際のサーバを走らせずにアプリケーションへのリクエストを作成します。FlaskのクライアントはWerkzeugのクライアントを拡張しており、追加情報はWerkzeugのドキュメントを見てください。 The test client makes requests to the application without running a live server. Flask's client extends :doc:`Werkzeug's client <werkzeug:test>`, see those docs for additional information.

clientは、client.get()client.post()のように、一般的なHTTPリクエストのメソッドに合致するメソッドを持っています。それらはリクエストを構築するために多くの引数を取ります: 全てのドキュメントはEnvironBuilderで見つけることができます。典型的には、pathquery_stringheaders、そしてdataまたはjsonを引数で使うでしょう。 The ``client`` has methods that match the common HTTP request methods, such as ``client.get()`` and ``client.post()``. They take many arguments for building the request; you can find the full documentation in :class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``, ``query_string``, ``headers``, and ``data`` or ``json``.

リクエストを作成するときは、テストする経路(route)へのpathと一緒に、リクエストが使うはずのメソッドを呼び出します。レスポンスのデータを調べるためにTestResponseが返されます。それはレスポンスのオブジェクトの通常の属性(properties)をすべて持っています。普通は、viewにより返されるbytesである、response.dataを調べるでしょう。もしテキストを使いたいときは、Werkzeug 2.1はresponse.textを提供しており、またはresponse.get_data(as_text=True)を使いましょう。 To make a request, call the method the request should use with the path to the route to test. A :class:`~werkzeug.test.TestResponse` is returned to examine the response data. It has all the usual properties of a response object. You'll usually look at ``response.data``, which is the bytes returned by the view. If you want to use text, Werkzeug 2.1 provides ``response.text``, or use ``response.get_data(as_text=True)``.

def test_request_example(client):
    response = client.get("/posts")
    assert b"<h2>Hello, World!</h2>" in response.data

query string(URLの?の後)の中の引数を設定するには、dictをquery_string={"key": "value", ...}のように引数で渡します。リクエストのヘッダーを設定するには、dictをheaders={}のように引数で渡します。 Pass a dict ``query_string={"key": "value", ...}`` to set arguments in the query string (after the ``?`` in the URL). Pass a dict ``headers={}`` to set request headers.

POSTまたはPUTリクエストの中でリクエストのbodyを送るには、data引数に値を渡します。もし手を加えていないbytes(raw bytes)を渡された場合、そのままのbodyが使われます。普通はformのデータを設定するにはdictを渡すでしょう。 To send a request body in a POST or PUT request, pass a value to ``data``. If raw bytes are passed, that exact body is used. Usually, you'll pass a dict to set form data.

Formのデータ Form Data

formのデータを渡すには、data引数にdictを渡します。Content-Typeヘッダーはmultipart/form-dataまたはapplication/x-www-form-urlencodedへ自動的に設定されるでしょう。 To send form data, pass a dict to ``data``. The ``Content-Type`` header will be set to ``multipart/form-data`` or ``application/x-www-form-urlencoded`` automatically.

もし値(訳注: data引数に渡されたdictに含まれる値)がbytesを読み取るために開かれたfileオブジェクト("rb"モード)の場合、それはアップロードされたファイルとして扱われます。検知されたファイル名とcontent typeを変更するには、tupleの(file, filename, content_type)を渡します(訳注: 「file: (open(filename, "rb"), filename, content_type)」のように、dict内のアイテムで該当するtupleを値に設定します)。Fileオブジェクトは、通常のwith open() as f:パターンを使う必要が無いように、リクエストを作成した後に閉じられます。 If a value is a file object opened for reading bytes (``"rb"`` mode), it will be treated as an uploaded file. To change the detected filename and content type, pass a ``(file, filename, content_type)`` tuple. File objects will be closed after making the request, so they do not need to use the usual ``with open() as f:`` pattern.

(以下のコード例のように)``tests/resources``フォルダにファイルを格納し、それからpathlib.pathを使ってそのときのテストのファイルから相対的に(相対パスで)ファイルを取得できると、便利な場合があります。 It can be useful to store files in a ``tests/resources`` folder, then use ``pathlib.Path`` to get files relative to the current test file.

from pathlib import Path

# get the resources folder in the tests folder
resources = Path(__file__).parent / "resources"

def test_edit_user(client):
    response = client.post("/user/2/edit", data={
        "name": "Flask",
        "theme": "dark",
        "picture": (resources / "picture.png").open("rb"),
    })
    assert response.status_code == 200

JSONのデータ JSON Data

JSONのデータを送るには、json引数にオブジェクトを渡します。Content-Typeヘッダーは自動的にapplication/jsonに設定されます。 To send JSON data, pass an object to ``json``. The ``Content-Type`` header will be set to ``application/json`` automatically.

同様に、もしレスポンスがJSONのデータを含む場合、response.json属性はシリアライズを元に戻したオブジェクトが含まれます。 Similarly, if the response contains JSON data, the ``response.json`` attribute will contain the deserialized object.

def test_json_data(client):
    response = client.post("/graphql", json={
        "query": """
            query User($id: String!) {
                user(id: $id) {
                    name
                    theme
                    picture_url
                }
            }
        """,
        variables={"id": 2},
    })
    assert response.json["data"]["user"]["name"] == "Flask"

リダイレクトのフォロー Following Redirects

標準設定では、クライアントはレスポンスが転送(redirect)である場合に追加のリクエストを作成はしません。follow_redirects=Trueをリクエストのメソッドに渡すことで(訳注: 「client.get(path, follow_redirects=True)」など、テスト用クライアントがリクエストを作成するメソッドの引数に渡すことで)、クライアントは転送されない(non-redirect)レスポンスが返されるまでリクエストを作成し続けます。 By default, the client does not make additional requests if the response is a redirect. By passing ``follow_redirects=True`` to a request method, the client will continue to make requests until a non-redirect response is returned.

TestResponse.history属性は最後のレスポンスに至るまでのレスポンスのtupleです。各レスポンスは、そのレスポンスを生成したリクエストを記録しているrequest属性を持ちます。 :attr:`TestResponse.history <werkzeug.test.TestResponse.history>` is a tuple of the responses that led up to the final response. Each response has a :attr:`~werkzeug.test.TestResponse.request` attribute which records the request that produced that response.

def test_logout_redirect(client):
    response = client.get("/logout")
    # Check that there was one redirect response.
    assert len(response.history) == 1
    # Check that the second request was to the index page.
    assert response.request.path == "/index"

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

Flaskのコンテキストの変数、主にsession、へアクセスするにはwith文の中でクライアントを使います(訳注: コンテキストの変数は、実体がアプリケーションのコンテキスト(The Application Context)およびリクエストのコンテキスト(The Request Context)に格納され、主にcurrent_appやgなどの代理になるオブジェクトによりモジュール内でグローバルなスコープでアクセスできるもののような意味合い)。appとrequestのcontextは、withブロックを終了するまで、リクエストを作成したも活動状態(active)のまま残ります。 To access Flask's context variables, mainly :data:`~flask.session`, use the client in a ``with`` statement. The app and request context will remain active *after* making a request, until the ``with`` block ends.

from flask import session

def test_access_session(client):
    with client:
        client.post("/auth/login", data={"username": "flask"})
        # session is still accessible
        assert session["user_id"] == 1

    # session is no longer accessible

もしリクエストを作成するにセッション内のデータに対してアクセスまたは設定したい場合、クライアントのsession_transaciton()メソッドをwith文の中で使用します。それはsessionオブジェクトを返し、withブロックが終わるとsessionを保存します。 If you want to access or set a value in the session *before* making a request, use the client's :meth:`~flask.testing.FlaskClient.session_transaction` method in a ``with`` statement. It returns a session object, and will save the session once the block ends.

from flask import session

def test_modify_session(client):
    with client.session_transaction() as session:
        # set a user id without going through the login route
        session["user_id"] = 1

    # session is saved now

    response = client.get("/users/me")
    assert response.json["username"] == "flask"

CLI実行プログラムを使ったコマンドの実行 Running Commands with the CLI Runner

FlaskはFlaskCliRunnerを作成するtest_cli_runner()を提供し、それはCLIコマンドを隔離して走らせ出力をResultオブジェクトの中に捉えます。FlaskのCLI実行プログラムはClickのテスト用CLI実行プログラムを拡張したものであり、追加情報はそのドキュメントを見てください。 Flask provides :meth:`~flask.Flask.test_cli_runner` to create a :class:`~flask.testing.FlaskCliRunner`, which runs CLI commands in isolation and captures the output in a :class:`~click.testing.Result` object. Flask's runner extends :doc:`Click's runner <click:testing>`, see those docs for additional information.

コマンドラインからflaskコマンドを使って呼び出された場合と同じようにコマンドを呼び出すには、CLI実行プログラムのinvoke()メソッドを使用します。 Use the runner's :meth:`~flask.testing.FlaskCliRunner.invoke` method to call commands in the same way they would be called with the ``flask`` command 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_command(runner):
    result = runner.invoke(args="hello")
    assert "World" in result.output

    result = runner.invoke(args=["hello", "--name", "Flask"])
    assert "Flask" in result.output

活動状態(active)なコンテキストに依存したテスト(Tests that depend on an Active Context) Tests that depend on an Active Context

requestsession、またはcurrent_appへアクセスするために、application contextまたはrequest contextが活動状態(active)であることを想定しているviewまたはコマンドから呼び出される関数があるかもしれません。リクエストを作成するか、またはコマンドを呼び出してそれらをテストするよりも、コンテキストを直接作成して活動状態(active)にできます。 You may have functions that are called from views or commands, that expect an active :doc:`application context </appcontext>` or :doc:`request context </reqcontext>` because they access ``request``, ``session``, or ``current_app``. Rather than testing them by making a request or invoking the command, you can create and activate a context directly.

applicaiton contextをpushするにはwith app.app_context()を使います。例えば、データベース用のFlask拡張はデータベースへのクエリーを作成するために、普通はactiveなapplication contextを必要とします。 Use ``with app.app_context()`` to push an application context. For example, database extensions usually require an active app context to make queries.

def test_db_post_model(app):
    with app.app_context():
        post = db.session.query(Post).get(1)

request contextをpushするにはwith app.test_request_context()を使います。それは、クライアントのリクエストのメソッドと同じ引数を取ります。 Use ``with app.test_request_context()`` to push a request context. It takes the same arguments as the test client's request methods.

def test_validate_user_edit(app):
    with app.test_request_context(
        "/user/2/edit", method="POST", data={"name": ""}
    ):
        # call a function that accesses `request`
        messages = validate_edit_user()

    assert messages["name"][0] == "Name cannot be empty."

テスト用のrequest contextの作成では、Flaskのリクエスト振り分け(dispatching)のコードを何も走らせないため、before_request関数は呼び出されません。もしそれらを呼び出す必要があるときは、普通は完全なリクエストを代わりに作成した方が良いでしょう。しかしながら、それらを手動で呼び出すことは可能です。 Creating a test request context doesn't run any of the Flask dispatching code, so ``before_request`` functions are not called. If you need to call these, usually it's better to make a full request instead. However, it's possible to call them manually.

def test_auth_token(app):
    with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}):
        app.preprocess_request()
        assert g.user.name == "Flask"