Browse Source

For Issue #2286: Replaces references to unittest in the documentation with pytest

pull/2307/head
Neil Grey 8 years ago
parent
commit
65fc888172
  1. 176
      docs/testing.rst
  2. 2
      flask/app.py

176
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,89 @@ 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
import pytest
from flaskr import flaskr
class FlaskrTestCase(unittest.TestCase): @pytest.fixture
def client(request):
def setUp(self): db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() flaskr.app.config['TESTING'] = True
flaskr.app.testing = True client = flaskr.app.test_client()
self.app = flaskr.app.test_client()
with flaskr.app.app_context(): with flaskr.app.app_context():
flaskr.init_db() flaskr.init_db()
def tearDown(self): def teardown():
os.close(self.db_fd) os.close(db_fd)
os.unlink(flaskr.app.config['DATABASE']) os.unlink(flaskr.app.config['DATABASE'])
request.addfinalizer(teardown)
if __name__ == '__main__': return client
unittest.main()
The code in the :meth:`~unittest.TestCase.setUp` method creates a new test This client fixture will be called by each individual test. It gives us a
client and initializes a new database. This function is called before simple interface to the application, where we can trigger test requests to the
each individual test function is run. To delete the database after the application. The client will also keep track of cookies for us.
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 During setup, the ``TESTING`` config flag is activated. What
trigger test requests to the application, and the client will also keep track this does is disable error catching during request handling, so that
of cookies for us. you get better error reports when performing test requests against the
application.
Because SQLite3 is filesystem-based we can easily use the tempfile module 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, we close the file and remove it
from the filesystem in the
:func:`teardown` function.
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 setUp(self): def test_empty_db(client):
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() """Start with a blank database."""
flaskr.app.testing = True rv = client.get('/')
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 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 +126,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 +145,45 @@ 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): def logout(client):
return self.app.get('/logout', follow_redirects=True) 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"""
rv = login(client, flaskr.app.config['USERNAME'],
flaskr.app.config['PASSWORD'])
assert b'You were logged in' in rv.data assert b'You were logged in' in rv.data
rv = self.logout() rv = logout(client)
assert b'You were logged out' in rv.data assert b'You were logged out' in rv.data
rv = self.login('adminx', 'default') rv = login(client, flaskr.app.config['USERNAME'] + 'x',
flaskr.app.config['PASSWORD'])
assert b'Invalid username' in rv.data assert b'Invalid username' in rv.data
rv = self.login('admin', 'defaultx') rv = login(client, flaskr.app.config['USERNAME'],
flaskr.app.config['PASSWORD'] + 'x')
assert b'Invalid password' in rv.data 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,12 +196,17 @@ 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
...
---------------------------------------------------------------------- ================ test session starts ================
Ran 3 tests in 0.332s 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
OK ============= 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

2
flask/app.py

@ -222,7 +222,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…
Cancel
Save