mirror of https://github.com/mitsuhiko/flask.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
6.8 KiB
197 lines
6.8 KiB
.. _testing: |
|
|
|
Testing Flask Applications |
|
========================== |
|
|
|
**Something that is untested is broken.** |
|
|
|
Not sure where that is coming from, and it's not entirely correct, but |
|
also not that far from the truth. Untested applications make it hard to |
|
improve existing code and developers of untested applications tend to |
|
become pretty paranoid. If an application however has automated tests, you |
|
can safely change things and you will instantly know if your change broke |
|
something. |
|
|
|
Flask gives you a couple of ways to test applications. It mainly does |
|
that by exposing the Werkzeug test :class:`~werkzeug.Client` class to your |
|
code and handling the context locals for you. You can then use that with |
|
your favourite testing solution. In this documentation we will use the |
|
:mod:`unittest` package that comes preinstalled with each Python |
|
installation. |
|
|
|
The Application |
|
--------------- |
|
|
|
First we need an application to test for functionality. For the testing |
|
we will use the application from the :ref:`tutorial`. If you don't have |
|
that application yet, get the sources from `the examples`_. |
|
|
|
.. _the examples: |
|
http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ |
|
|
|
The Testing Skeleton |
|
-------------------- |
|
|
|
In order to test that, we add a second module ( |
|
`flaskr_tests.py`) and create a unittest skeleton there:: |
|
|
|
import os |
|
import flaskr |
|
import unittest |
|
import tempfile |
|
|
|
class FlaskrTestCase(unittest.TestCase): |
|
|
|
def setUp(self): |
|
self.db_fd, flaskr.DATABASE = tempfile.mkstemp() |
|
self.app = flaskr.app.test_client() |
|
flaskr.init_db() |
|
|
|
def tearDown(self): |
|
os.close(self.db_fd) |
|
os.unlink(flaskr.DATABASE) |
|
|
|
if __name__ == '__main__': |
|
unittest.main() |
|
|
|
The code in the :meth:`~unittest.TestCase.setUp` method creates a new test |
|
client and initializes a new database. That function is called before |
|
each individual test function. To delete the database after the test, we |
|
close the file and remove it from the filesystem in the |
|
:meth:`~unittest.TestCase.tearDown` method. What the test client does is |
|
give us a simple interface to the application. We can 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 |
|
to create a temporary database and initialize it. The |
|
: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 |
|
database name. We just have to keep the `db_fd` around so that we can use |
|
the :func:`os.close` function to close the function. |
|
|
|
If we now run that testsuite, we should see the following output:: |
|
|
|
$ python flaskr_tests.py |
|
|
|
---------------------------------------------------------------------- |
|
Ran 0 tests in 0.000s |
|
|
|
OK |
|
|
|
Even though it did not run any tests, we already know that our flaskr |
|
application is syntactically valid, otherwise the import would have died |
|
with an exception. |
|
|
|
The First Test |
|
-------------- |
|
|
|
Now we can add the first test. Let's check that the application shows |
|
"No entries here so far" if we access the root of the application (``/``). |
|
For that we modify our created test case class so that it looks like |
|
this:: |
|
|
|
class FlaskrTestCase(unittest.TestCase): |
|
|
|
def setUp(self): |
|
self.db_fd, flaskr.DATABASE = tempfile.mkstemp() |
|
self.app = flaskr.app.test_client() |
|
flaskr.init_db() |
|
|
|
def tearDown(self): |
|
os.close(self.db_fd) |
|
os.unlink(flaskr.DATABASE) |
|
|
|
def test_empty_db(self): |
|
rv = self.app.get('/') |
|
assert 'No entries here so far' in rv.data |
|
|
|
Test functions begin with the word `test`. Every function named like that |
|
will be picked up automatically. By using `self.app.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. We can now |
|
use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the |
|
return value (as string) from the application. In this case, we ensure |
|
that ``'No entries here so far'`` is part of the output. |
|
|
|
Run it again and you should see one passing test:: |
|
|
|
$ python flaskr_tests.py |
|
. |
|
---------------------------------------------------------------------- |
|
Ran 1 test in 0.034s |
|
|
|
OK |
|
|
|
Of course you can submit forms with the test client as well, which we will |
|
use now to log our user in. |
|
|
|
Logging In and Out |
|
------------------ |
|
|
|
The majority of the functionality of our application is only available for |
|
the administration user. So we need a way to log our test client in to the |
|
application and out of it again. For that we fire some requests to the |
|
login and logout pages with the required form data (username and |
|
password). Because the login and logout pages redirect, we tell the |
|
client to `follow_redirects`. |
|
|
|
Add the following two methods to your `FlaskrTestCase` class:: |
|
|
|
def login(self, username, password): |
|
return self.app.post('/login', data=dict( |
|
username=username, |
|
password=password |
|
), follow_redirects=True) |
|
|
|
def logout(self): |
|
return self.app.get('/logout', follow_redirects=True) |
|
|
|
Now we can easily test if logging in and out works and that it fails with |
|
invalid credentials. Add this new test to the class:: |
|
|
|
def test_login_logout(self): |
|
rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) |
|
assert 'You were logged in' in rv.data |
|
rv = self.logout() |
|
assert 'You were logged out' in rv.data |
|
rv = self.login(flaskr.USERNAME + 'x', flaskr.PASSWORD) |
|
assert 'Invalid username' in rv.data |
|
rv = self.login(flaskr.USERNAME, flaskr.PASSWORD + 'x') |
|
assert 'Invalid password' in rv.data |
|
|
|
Test Adding Messages |
|
-------------------- |
|
|
|
Now we can also test that adding messages works. Add a new test method |
|
like this:: |
|
|
|
def test_messages(self): |
|
self.login(flaskr.USERNAME, flaskr.PASSWORD) |
|
rv = self.app.post('/add', data=dict( |
|
title='<Hello>', |
|
text='<strong>HTML</strong> allowed here' |
|
), follow_redirects=True) |
|
assert 'No entries here so far' not in rv.data |
|
assert '<Hello>' in rv.data |
|
assert '<strong>HTML</strong> allowed here' in rv.data |
|
|
|
Here we check that HTML is allowed in the text but not in the title, |
|
which is the intended behavior. |
|
|
|
Running that should now give us three passing tests:: |
|
|
|
$ python flaskr_tests.py |
|
... |
|
---------------------------------------------------------------------- |
|
Ran 3 tests in 0.332s |
|
|
|
OK |
|
|
|
For more complex tests with headers and status codes, check out the |
|
`MiniTwit Example`_ from the sources. That one contains a larger test |
|
suite. |
|
|
|
|
|
.. _MiniTwit Example: |
|
http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/
|
|
|