diff --git a/docs/testing.rst b/docs/testing.rst new file mode 100644 index 00000000..4c04414d --- /dev/null +++ b/docs/testing.rst @@ -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('/') + def hello(name='World'): + return render_template_string(''' + + Hello {{ name }}! +

Hello {{ name }}!

+ ''', 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/ diff --git a/examples/minitwit/README b/examples/minitwit/README index e47c8792..065674a9 100644 --- a/examples/minitwit/README +++ b/examples/minitwit/README @@ -10,7 +10,7 @@ ~ 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: diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 05f0689d..37e6cb5c 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -31,7 +31,7 @@ app = Flask(__name__) def connect_db(): - """Returns a new database connection to the database.""" + """Returns a new connection to the database.""" return sqlite3.connect(DATABASE) @@ -52,19 +52,19 @@ def query_db(query, args=(), one=False): 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 = ?', [username]).fetchone() return rv[0] if rv else None def format_datetime(timestamp): - """Format a timestamp for display""" + """Format a timestamp for display.""" return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') 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' % \ (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) @@ -138,7 +138,7 @@ def user_timeline(username): @app.route('//follow') 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: abort(401) whom_id = get_user_id(username) @@ -153,7 +153,7 @@ def follow_user(username): @app.route('//unfollow') 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: abort(401) whom_id = get_user_id(username) @@ -168,7 +168,7 @@ def unfollow_user(username): @app.route('/add_message', methods=['POST']) def add_message(): - """Registers a new message for the user""" + """Registers a new message for the user.""" if 'user_id' not in session: abort(401) if request.form['text']: @@ -182,7 +182,7 @@ def add_message(): @app.route('/login', methods=['GET', 'POST']) def login(): - """Logs the user in""" + """Logs the user in.""" if g.user: return redirect(url_for('timeline')) error = None @@ -203,7 +203,7 @@ def login(): @app.route('/register', methods=['GET', 'POST']) def register(): - """Registers the user""" + """Registers the user.""" if g.user: return redirect(url_for('timeline')) error = None diff --git a/examples/minitwit/templates/timeline.html b/examples/minitwit/templates/timeline.html index 892b8fcc..ea7d751b 100644 --- a/examples/minitwit/templates/timeline.html +++ b/examples/minitwit/templates/timeline.html @@ -43,7 +43,7 @@ {{ message.text }} — {{ message.pub_date|datetimeformat }} {% else %} -
  • There are no messages so far. +
  • There's no message so far. {% endfor %} {% endblock %}