forked from orbit-oss/flask
Merge pull request #2307 from neilvictorgrey/master
For Issue #2286: Update unittest references
This commit is contained in:
commit
cd593bf117
4 changed files with 117 additions and 101 deletions
|
|
@ -27,7 +27,7 @@ executed in undefined order and do not modify any data.
|
||||||
|
|
||||||
The big advantage of signals over handlers is that you can safely
|
The big advantage of signals over handlers is that you can safely
|
||||||
subscribe to them for just a split second. These temporary
|
subscribe to them for just a split second. These temporary
|
||||||
subscriptions are helpful for unittesting for example. Say you want to
|
subscriptions are helpful for unit testing for example. Say you want to
|
||||||
know what templates were rendered as part of a request: signals allow you
|
know what templates were rendered as part of a request: signals allow you
|
||||||
to do exactly that.
|
to do exactly that.
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ signal. When you subscribe to a signal, be sure to also provide a sender
|
||||||
unless you really want to listen for signals from all applications. This is
|
unless you really want to listen for signals from all applications. This is
|
||||||
especially true if you are developing an extension.
|
especially true if you are developing an extension.
|
||||||
|
|
||||||
For example, here is a helper context manager that can be used in a unittest
|
For example, here is a helper context manager that can be used in a unit test
|
||||||
to determine which templates were rendered and what variables were passed
|
to determine which templates were rendered and what variables were passed
|
||||||
to the template::
|
to the template::
|
||||||
|
|
||||||
|
|
|
||||||
202
docs/testing.rst
202
docs/testing.rst
|
|
@ -5,23 +5,30 @@ Testing Flask Applications
|
||||||
|
|
||||||
**Something that is untested is broken.**
|
**Something that is untested is broken.**
|
||||||
|
|
||||||
The origin of this quote is unknown and while it is not entirely correct, it is also
|
The origin of this quote is unknown and while it is not entirely correct, it
|
||||||
not far from the truth. Untested applications make it hard to
|
is also not far from the truth. Untested applications make it hard to
|
||||||
improve existing code and developers of untested applications tend to
|
improve existing code and developers of untested applications tend to
|
||||||
become pretty paranoid. If an application has automated tests, you can
|
become pretty paranoid. If an application has automated tests, you can
|
||||||
safely make changes and instantly know if anything breaks.
|
safely make changes and instantly know if anything breaks.
|
||||||
|
|
||||||
Flask provides a way to test your application by exposing the Werkzeug
|
Flask provides a way to test your application by exposing the Werkzeug
|
||||||
test :class:`~werkzeug.test.Client` and handling the context locals for you.
|
test :class:`~werkzeug.test.Client` and handling the context locals for you.
|
||||||
You can then use that with your favourite testing solution. In this documentation
|
You can then use that with your favourite testing solution.
|
||||||
we will use the :mod:`unittest` package that comes pre-installed with Python.
|
|
||||||
|
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
|
||||||
|
|
||||||
|
.. _pytest:
|
||||||
|
https://pytest.org
|
||||||
|
|
||||||
The Application
|
The Application
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
First, we need an application to test; we will use the application from
|
First, we need an application to test; we will use the application from
|
||||||
the :ref:`tutorial`. If you don't have that application yet, get the
|
the :ref:`tutorial`. If you don't have that application yet, get the
|
||||||
sources from `the examples`_.
|
source code from `the examples`_.
|
||||||
|
|
||||||
.. _the examples:
|
.. _the examples:
|
||||||
https://github.com/pallets/flask/tree/master/examples/flaskr/
|
https://github.com/pallets/flask/tree/master/examples/flaskr/
|
||||||
|
|
@ -29,92 +36,91 @@ sources from `the examples`_.
|
||||||
The Testing Skeleton
|
The Testing Skeleton
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
In order to test the application, we add a second module
|
We begin by adding a tests directory under the application root. Then
|
||||||
(:file:`flaskr_tests.py`) and create a unittest skeleton there::
|
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.
|
||||||
|
|
||||||
|
Next, we create a `pytest fixture`_ called
|
||||||
|
:func:`client` that configures
|
||||||
|
the application for testing and initializes a new database.::
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from flaskr import flaskr
|
|
||||||
import unittest
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
class FlaskrTestCase(unittest.TestCase):
|
import pytest
|
||||||
|
|
||||||
def setUp(self):
|
from flaskr import flaskr
|
||||||
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
|
|
||||||
flaskr.app.testing = True
|
|
||||||
self.app = flaskr.app.test_client()
|
|
||||||
with flaskr.app.app_context():
|
|
||||||
flaskr.init_db()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
os.close(self.db_fd)
|
|
||||||
os.unlink(flaskr.app.config['DATABASE'])
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
@pytest.fixture
|
||||||
unittest.main()
|
def client(request):
|
||||||
|
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
|
||||||
|
flaskr.app.config['TESTING'] = True
|
||||||
|
client = flaskr.app.test_client()
|
||||||
|
|
||||||
The code in the :meth:`~unittest.TestCase.setUp` method creates a new test
|
with flaskr.app.app_context():
|
||||||
client and initializes a new database. This function is called before
|
flaskr.init_db()
|
||||||
each individual test function is run. To delete the database after the
|
|
||||||
test, we close the file and remove it from the filesystem in the
|
|
||||||
:meth:`~unittest.TestCase.tearDown` method. Additionally during setup the
|
|
||||||
``TESTING`` config flag is activated. What it does is disable the error
|
|
||||||
catching during request handling so that you get better error reports when
|
|
||||||
performing test requests against the application.
|
|
||||||
|
|
||||||
This test client will give us a simple interface to the application. We can
|
yield client
|
||||||
trigger test requests to the application, and the client will also keep track
|
|
||||||
of cookies for us.
|
|
||||||
|
|
||||||
Because SQLite3 is filesystem-based we can easily use the tempfile module
|
os.close(db_fd)
|
||||||
|
os.unlink(flaskr.app.config['DATABASE'])
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Because SQLite3 is filesystem-based, we can easily use the :mod:`tempfile` module
|
||||||
to create a temporary database and initialize it. The
|
to create a temporary database and initialize it. The
|
||||||
:func:`~tempfile.mkstemp` function does two things for us: it returns a
|
: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
|
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
|
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.
|
the :func:`os.close` function to close the file.
|
||||||
|
|
||||||
|
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::
|
If we now run the test suite, we should see the following output::
|
||||||
|
|
||||||
$ python flaskr_tests.py
|
$ pytest
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
================ test session starts ================
|
||||||
Ran 0 tests in 0.000s
|
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
|
||||||
|
collected 0 items
|
||||||
|
|
||||||
OK
|
=========== no tests ran in 0.07 seconds ============
|
||||||
|
|
||||||
Even though it did not run any actual tests, we already know that our flaskr
|
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
|
application is syntactically valid, otherwise the import would have died
|
||||||
with an exception.
|
with an exception.
|
||||||
|
|
||||||
|
.. _pytest fixture:
|
||||||
|
https://docs.pytest.org/en/latest/fixture.html
|
||||||
|
|
||||||
The First Test
|
The First Test
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Now it's time to start testing the functionality of the application.
|
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
|
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
|
access the root of the application (``/``). To do this, we add a new
|
||||||
test method to our class, like this::
|
test function to :file:`test_flaskr.py`, like this::
|
||||||
|
|
||||||
class FlaskrTestCase(unittest.TestCase):
|
def test_empty_db(client):
|
||||||
|
"""Start with a blank database."""
|
||||||
|
|
||||||
def setUp(self):
|
rv = client.get('/')
|
||||||
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
|
assert b'No entries here so far' in rv.data
|
||||||
flaskr.app.testing = True
|
|
||||||
self.app = flaskr.app.test_client()
|
|
||||||
with flaskr.app.app_context():
|
|
||||||
flaskr.init_db()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
os.close(self.db_fd)
|
|
||||||
os.unlink(flaskr.app.config['DATABASE'])
|
|
||||||
|
|
||||||
def test_empty_db(self):
|
|
||||||
rv = self.app.get('/')
|
|
||||||
assert b'No entries here so far' in rv.data
|
|
||||||
|
|
||||||
Notice that our test functions begin with the word `test`; this allows
|
Notice that our test functions begin with the word `test`; this allows
|
||||||
:mod:`unittest` to automatically identify the method as a test to run.
|
`pytest`_ to automatically identify the function as a test to run.
|
||||||
|
|
||||||
By using `self.app.get` we can send an HTTP ``GET`` request to the application with
|
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.
|
the given path. The return value will be a :class:`~flask.Flask.response_class` object.
|
||||||
We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect
|
We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect
|
||||||
the return value (as string) from the application. In this case, we ensure that
|
the return value (as string) from the application. In this case, we ensure that
|
||||||
|
|
@ -122,12 +128,15 @@ the return value (as string) from the application. In this case, we ensure that
|
||||||
|
|
||||||
Run it again and you should see one passing test::
|
Run it again and you should see one passing test::
|
||||||
|
|
||||||
$ python flaskr_tests.py
|
$ pytest -v
|
||||||
.
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
Ran 1 test in 0.034s
|
|
||||||
|
|
||||||
OK
|
================ 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
|
Logging In and Out
|
||||||
------------------
|
------------------
|
||||||
|
|
@ -138,39 +147,47 @@ 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
|
pages with the required form data (username and password). And because the
|
||||||
login and logout pages redirect, we tell the client to `follow_redirects`.
|
login and logout pages redirect, we tell the client to `follow_redirects`.
|
||||||
|
|
||||||
Add the following two methods to your `FlaskrTestCase` class::
|
Add the following two functions to your :file:`test_flaskr.py` file::
|
||||||
|
|
||||||
def login(self, username, password):
|
def login(client, username, password):
|
||||||
return self.app.post('/login', data=dict(
|
return client.post('/login', data=dict(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
), follow_redirects=True)
|
), follow_redirects=True)
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return self.app.get('/logout', follow_redirects=True)
|
def logout(client):
|
||||||
|
return client.get('/logout', follow_redirects=True)
|
||||||
|
|
||||||
Now we can easily test that logging in and out works and that it fails with
|
Now we can easily test that logging in and out works and that it fails with
|
||||||
invalid credentials. Add this new test to the class::
|
invalid credentials. Add this new test function::
|
||||||
|
|
||||||
def test_login_logout(self):
|
def test_login_logout(client):
|
||||||
rv = self.login('admin', 'default')
|
"""Make sure login and logout works."""
|
||||||
assert b'You were logged in' in rv.data
|
|
||||||
rv = self.logout()
|
rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
|
||||||
assert b'You were logged out' in rv.data
|
assert b'You were logged in' in rv.data
|
||||||
rv = self.login('adminx', 'default')
|
|
||||||
assert b'Invalid username' in rv.data
|
rv = logout(client)
|
||||||
rv = self.login('admin', 'defaultx')
|
assert b'You were logged out' in rv.data
|
||||||
assert b'Invalid password' 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
|
||||||
|
|
||||||
Test Adding Messages
|
Test Adding Messages
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
We should also test that adding messages works. Add a new test method
|
We should also test that adding messages works. Add a new test function
|
||||||
like this::
|
like this::
|
||||||
|
|
||||||
def test_messages(self):
|
def test_messages(client):
|
||||||
self.login('admin', 'default')
|
"""Test that messages work."""
|
||||||
rv = self.app.post('/add', data=dict(
|
|
||||||
|
login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
|
||||||
|
rv = client.post('/add', data=dict(
|
||||||
title='<Hello>',
|
title='<Hello>',
|
||||||
text='<strong>HTML</strong> allowed here'
|
text='<strong>HTML</strong> allowed here'
|
||||||
), follow_redirects=True)
|
), follow_redirects=True)
|
||||||
|
|
@ -183,22 +200,25 @@ which is the intended behavior.
|
||||||
|
|
||||||
Running that should now give us three passing tests::
|
Running that should now give us three passing tests::
|
||||||
|
|
||||||
$ python flaskr_tests.py
|
$ pytest -v
|
||||||
...
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
Ran 3 tests in 0.332s
|
|
||||||
|
|
||||||
OK
|
================ 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 ==============
|
||||||
|
|
||||||
For more complex tests with headers and status codes, check out the
|
For more complex tests with headers and status codes, check out the
|
||||||
`MiniTwit Example`_ from the sources which contains a larger test
|
`MiniTwit Example`_ from the sources which contains a larger test
|
||||||
suite.
|
suite.
|
||||||
|
|
||||||
|
|
||||||
.. _MiniTwit Example:
|
.. _MiniTwit Example:
|
||||||
https://github.com/pallets/flask/tree/master/examples/minitwit/
|
https://github.com/pallets/flask/tree/master/examples/minitwit/
|
||||||
|
|
||||||
|
|
||||||
Other Testing Tricks
|
Other Testing Tricks
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,9 @@ def client(request):
|
||||||
client = flaskr.app.test_client()
|
client = flaskr.app.test_client()
|
||||||
with flaskr.app.app_context():
|
with flaskr.app.app_context():
|
||||||
flaskr.init_db()
|
flaskr.init_db()
|
||||||
|
yield client
|
||||||
def teardown():
|
os.close(db_fd)
|
||||||
os.close(db_fd)
|
os.unlink(flaskr.app.config['DATABASE'])
|
||||||
os.unlink(flaskr.app.config['DATABASE'])
|
|
||||||
request.addfinalizer(teardown)
|
|
||||||
|
|
||||||
return client
|
|
||||||
|
|
||||||
|
|
||||||
def login(client, username, password):
|
def login(client, username, password):
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ class Flask(_PackageBoundObject):
|
||||||
|
|
||||||
#: The testing flag. Set this to ``True`` to enable the test mode of
|
#: The testing flag. Set this to ``True`` to enable the test mode of
|
||||||
#: Flask extensions (and in the future probably also Flask itself).
|
#: Flask extensions (and in the future probably also Flask itself).
|
||||||
#: For example this might activate unittest helpers that have an
|
#: For example this might activate test helpers that have an
|
||||||
#: additional runtime cost which should not be enabled by default.
|
#: additional runtime cost which should not be enabled by default.
|
||||||
#:
|
#:
|
||||||
#: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the
|
#: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue