forked from orbit-oss/flask
Doc updates and typo fixes
This commit is contained in:
parent
03148dba6b
commit
2f5a4f8dbc
4 changed files with 127 additions and 11 deletions
116
docs/testing.rst
Normal file
116
docs/testing.rst
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
.. _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 savely 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 us the
|
||||||
|
:mod:`unittest` package that comes preinstalled with each Python
|
||||||
|
installation.
|
||||||
|
|
||||||
|
The Application
|
||||||
|
---------------
|
||||||
|
|
||||||
|
First we need an application to test for functionality. Let's start
|
||||||
|
simple with a Hello World application (`hello.py`)::
|
||||||
|
|
||||||
|
from flask import Flask, render_template_string
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
@app.route('/<name>')
|
||||||
|
def hello(name='World'):
|
||||||
|
return render_template_string('''
|
||||||
|
<!doctype html>
|
||||||
|
<title>Hello {{ name }}!</title>
|
||||||
|
<h1>Hello {{ name }}!</h1>
|
||||||
|
''', name=name)
|
||||||
|
|
||||||
|
The Testing Skeleton
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
In order to test that, we add a second module (
|
||||||
|
`hello_tests.py`) and create a unittest skeleton there::
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import hello
|
||||||
|
|
||||||
|
class HelloWorldTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = hello.app.test_client()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
||||||
|
The code in the `setUp` function creates a new test client. That function
|
||||||
|
is called before each individual test function. What the test client does
|
||||||
|
for us is giving 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.
|
||||||
|
|
||||||
|
If we now run that testsuite, we should see the following output::
|
||||||
|
|
||||||
|
$ python hello_tests.py
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Ran 0 tests in 0.000s
|
||||||
|
|
||||||
|
OK
|
||||||
|
|
||||||
|
Even though it did not run any tests, we already know that our hello
|
||||||
|
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 greets us
|
||||||
|
with "Hello World" if we access it on ``/``. For that we modify our
|
||||||
|
created test case class so that it looks like this::
|
||||||
|
|
||||||
|
class HelloWorldTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = hello.app.test_client()
|
||||||
|
|
||||||
|
def test_hello_world(self):
|
||||||
|
rv = self.app.get('/')
|
||||||
|
assert 'Hello World!' 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 ``'Hello World!'`` is part of the output.
|
||||||
|
|
||||||
|
Run it again and you should see one passing test. Let's add a second test
|
||||||
|
here::
|
||||||
|
|
||||||
|
def test_hello_name(self):
|
||||||
|
rv = self.app.get('/Peter')
|
||||||
|
assert 'Hello Peter!' in rv.data
|
||||||
|
|
||||||
|
Of course you can submit forms with the test client as well. For that and
|
||||||
|
other features of the test client, check the documentation of the Werkzeug
|
||||||
|
test :class:`~werkzeug.Client` and the tests of the MiniTwit example
|
||||||
|
application:
|
||||||
|
|
||||||
|
- Werkzeug Test :class:`~werkzeug.Client`
|
||||||
|
- `MiniTwit Example`_
|
||||||
|
|
||||||
|
.. _MiniTwit Example:
|
||||||
|
http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
~ How do I use it?
|
~ How do I use it?
|
||||||
|
|
||||||
1. edit the configurtion in the minitwit.py file
|
1. edit the configuration in the minitwit.py file
|
||||||
|
|
||||||
2. fire up a python shell and run this:
|
2. fire up a python shell and run this:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
def connect_db():
|
def connect_db():
|
||||||
"""Returns a new database connection to the database."""
|
"""Returns a new connection to the database."""
|
||||||
return sqlite3.connect(DATABASE)
|
return sqlite3.connect(DATABASE)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -52,19 +52,19 @@ def query_db(query, args=(), one=False):
|
||||||
|
|
||||||
|
|
||||||
def get_user_id(username):
|
def get_user_id(username):
|
||||||
"""Convenience method to look up the id for a username"""
|
"""Convenience method to look up the id for a username."""
|
||||||
rv = g.db.execute('select user_id from user where username = ?',
|
rv = g.db.execute('select user_id from user where username = ?',
|
||||||
[username]).fetchone()
|
[username]).fetchone()
|
||||||
return rv[0] if rv else None
|
return rv[0] if rv else None
|
||||||
|
|
||||||
|
|
||||||
def format_datetime(timestamp):
|
def format_datetime(timestamp):
|
||||||
"""Format a timestamp for display"""
|
"""Format a timestamp for display."""
|
||||||
return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M')
|
return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M')
|
||||||
|
|
||||||
|
|
||||||
def gravatar_url(email, size=80):
|
def gravatar_url(email, size=80):
|
||||||
"""Return the gravatar image for the given email address"""
|
"""Return the gravatar image for the given email address."""
|
||||||
return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \
|
return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \
|
||||||
(md5(email.strip().lower().encode('utf-8')).hexdigest(), size)
|
(md5(email.strip().lower().encode('utf-8')).hexdigest(), size)
|
||||||
|
|
||||||
|
|
@ -138,7 +138,7 @@ def user_timeline(username):
|
||||||
|
|
||||||
@app.route('/<username>/follow')
|
@app.route('/<username>/follow')
|
||||||
def follow_user(username):
|
def follow_user(username):
|
||||||
"""Adds the current user as follower of the given user"""
|
"""Adds the current user as follower of the given user."""
|
||||||
if not g.user:
|
if not g.user:
|
||||||
abort(401)
|
abort(401)
|
||||||
whom_id = get_user_id(username)
|
whom_id = get_user_id(username)
|
||||||
|
|
@ -153,7 +153,7 @@ def follow_user(username):
|
||||||
|
|
||||||
@app.route('/<username>/unfollow')
|
@app.route('/<username>/unfollow')
|
||||||
def unfollow_user(username):
|
def unfollow_user(username):
|
||||||
"""Removes the current user as follower of the given user"""
|
"""Removes the current user as follower of the given user."""
|
||||||
if not g.user:
|
if not g.user:
|
||||||
abort(401)
|
abort(401)
|
||||||
whom_id = get_user_id(username)
|
whom_id = get_user_id(username)
|
||||||
|
|
@ -168,7 +168,7 @@ def unfollow_user(username):
|
||||||
|
|
||||||
@app.route('/add_message', methods=['POST'])
|
@app.route('/add_message', methods=['POST'])
|
||||||
def add_message():
|
def add_message():
|
||||||
"""Registers a new message for the user"""
|
"""Registers a new message for the user."""
|
||||||
if 'user_id' not in session:
|
if 'user_id' not in session:
|
||||||
abort(401)
|
abort(401)
|
||||||
if request.form['text']:
|
if request.form['text']:
|
||||||
|
|
@ -182,7 +182,7 @@ def add_message():
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
"""Logs the user in"""
|
"""Logs the user in."""
|
||||||
if g.user:
|
if g.user:
|
||||||
return redirect(url_for('timeline'))
|
return redirect(url_for('timeline'))
|
||||||
error = None
|
error = None
|
||||||
|
|
@ -203,7 +203,7 @@ def login():
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
"""Registers the user"""
|
"""Registers the user."""
|
||||||
if g.user:
|
if g.user:
|
||||||
return redirect(url_for('timeline'))
|
return redirect(url_for('timeline'))
|
||||||
error = None
|
error = None
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
{{ message.text }}
|
{{ message.text }}
|
||||||
<small>— {{ message.pub_date|datetimeformat }}</small>
|
<small>— {{ message.pub_date|datetimeformat }}</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><em>There are no messages so far.</em>
|
<li><em>There's no message so far.</em>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue