mirror of https://github.com/pallets/flask.git
320 lines
10 KiB
ReStructuredText
320 lines
10 KiB
ReStructuredText
Testing Flask Applications
|
|
==========================
|
|
|
|
Flask provides utilities for testing an application. This documentation
|
|
goes over techniques for working with different parts of the application
|
|
in tests.
|
|
|
|
We will use the `pytest`_ framework to set up and run our tests.
|
|
|
|
.. code-block:: text
|
|
|
|
$ pip install pytest
|
|
|
|
.. _pytest: https://docs.pytest.org/
|
|
|
|
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 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
|
|
--------
|
|
|
|
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``.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
.. code-block:: python
|
|
|
|
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
|
|
-------------------------------------
|
|
|
|
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.
|
|
|
|
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``.
|
|
|
|
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)``.
|
|
|
|
.. code-block:: python
|
|
|
|
def test_request_example(client):
|
|
response = client.get("/posts")
|
|
assert b"<h2>Hello, World!</h2>" in response.data
|
|
|
|
|
|
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.
|
|
|
|
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 Data
|
|
~~~~~~~~~
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
.. code-block:: python
|
|
|
|
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 Data
|
|
~~~~~~~~~
|
|
|
|
To send JSON data, pass an object to ``json``. The ``Content-Type``
|
|
header will be set to ``application/json`` automatically.
|
|
|
|
Similarly, if the response contains JSON data, the ``response.json``
|
|
attribute will contain the deserialized object.
|
|
|
|
.. code-block:: python
|
|
|
|
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
|
|
-------------------
|
|
|
|
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.
|
|
|
|
: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.
|
|
|
|
.. code-block:: python
|
|
|
|
def test_logout_redirect(client):
|
|
response = client.get("/logout", follow_redirects=True)
|
|
# 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
|
|
-----------------------------------
|
|
|
|
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.
|
|
|
|
.. code-block:: python
|
|
|
|
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
|
|
|
|
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.
|
|
|
|
.. code-block:: python
|
|
|
|
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"
|
|
|
|
|
|
.. _testing-cli:
|
|
|
|
Running Commands with the CLI Runner
|
|
------------------------------------
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
.. code-block:: python
|
|
|
|
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
|
|
|
|
|
|
Tests that depend on an Active Context
|
|
--------------------------------------
|
|
|
|
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.
|
|
|
|
Use ``with app.app_context()`` to push an application context. For
|
|
example, database extensions usually require an active app context to
|
|
make queries.
|
|
|
|
.. code-block:: python
|
|
|
|
def test_db_post_model(app):
|
|
with app.app_context():
|
|
post = db.session.query(Post).get(1)
|
|
|
|
Use ``with app.test_request_context()`` to push a request context. It
|
|
takes the same arguments as the test client's request methods.
|
|
|
|
.. code-block:: python
|
|
|
|
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."
|
|
|
|
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.
|
|
|
|
.. code-block:: python
|
|
|
|
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"
|