File: testing.rst

package info (click to toggle)
flask 3.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,536 kB
  • sloc: python: 10,083; makefile: 32; sql: 22; sh: 19
file content (319 lines) | stat: -rw-r--r-- 10,448 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
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"