Unit Testing with paste.fixture
Pylons provides powerful unit testing capabilities for your web application utilizing paste.fixture to emulate requests to your web application. You can then ensure that the response was handled appropriately and that the controller set things up properly.
To run the test suite for your web application, Pylons utilizes the nose test runner/discovery package. Running nosetests in your project directory will run all the tests you create in the tests directory. If you don't have nose installed on your system, it can be installed via setuptools with:
1 | $ easy_install -U nose
|
To avoid conflicts with your development setup, the tests use the test.ini configuration file when run. This means you must configure any databases, etc. in your test.ini file or your tests will not be able to find the database configuration.
Warning
Nose can trigger errors during its attempt to search for doc tests since it will try and import all your modules one at a time before your app was loaded. This will cause files under models/ that rely on your app to be running, to fail.
Pylons 0.9.6.1 and later includes a plugin for nose that loads the app before the doctests scan your modules, allowing models to be doctested. You can use this option from the command line with nose:
1 | nosetests --with-pylons=test.ini |
Or by setting up a [nosetests] block in your setup.cfg:
1 2 3 4 5 6 | [nosetests] verbose=True verbosity=2 with-pylons=test.ini detailed-errors=1 with-doctest=True |
Then just run:
1 | python setup.py nosetests |
to run the tests.
Example: Testing a Controller
First let's create a new project and controller for this example:
1 2 3 | $ paster create -t pylons TestExample $ cd TestExample $ paster controller comments |
You'll see that it creates two files when you create a controller. The stub controller, and a test for it under testexample/tests/functional/.
Modify the testexample/controllers/comments.py file so it looks like this:
1 2 3 4 5 6 7 8 9 10 | from testexample.lib.base import * class CommentsController(BaseController): def index(self): return 'Basic output' def sess(self): session['name'] = 'Joe Smith' session.save() return 'Saved a session' |
Then write a basic set of tests to ensure that the controller actions are functioning properly, modify testexample/tests/functional/test_comments.py to match the following:
1 2 3 4 5 6 7 8 9 10 11 | from testexample.tests import * class TestCommentsController(TestController): def test_index(self): response = self.app.get(url_for(controller='/comments')) assert 'Basic output' in response def test_sess(self): response = self.app.get(url_for(controller='/comments', action='sess')) assert response.session['name'] == 'Joe Smith' assert 'Saved a session' in response |
Run nosetests in your main project directory and you should see them all pass:
1 2 3 4 5 | .. ---------------------------------------------------------------------- Ran 2 tests in 2.999s OK |
Unfortunately, a plain assert does not provide detailed information about the results of an assertion should it fail, unless you specify it a second argument. For example, add the following test to the test_sess function:
1 | assert response.session.has_key('address') == True |
When you run nosetests you will get the following, not-very-helpful result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .F ====================================================================== FAIL: test_sess (testexample.tests.functional.test_comments.TestCommentsController) ---------------------------------------------------------------------- Traceback (most recent call last): File "~/TestExample/testexample/tests/functional/test_comments.py", line 12, in test_sess assert response.session.has_key('address') == True AssertionError: ---------------------------------------------------------------------- Ran 2 tests in 1.417s FAILED (failures=1) |
You can augment this result by doing the following:
1 | assert response.session.has_key('address') == True, "address not found in session" |
Which results in:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .F ====================================================================== FAIL: test_sess (testexample.tests.functional.test_comments.TestCommentsController) ---------------------------------------------------------------------- Traceback (most recent call last): File "~/TestExample/testexample/tests/functional/test_comments.py", line 12, in test_sess assert response.session.has_key('address') == True AssertionError: address not found in session ---------------------------------------------------------------------- Ran 2 tests in 1.417s FAILED (failures=1) |
But detailing every assert statement could be time consuming. Our TestController subclasses the standard Python unittest.TestCase class, so we can use utilize its helper methods, such as assertEqual, that can automatically provide a more detailed AssertionError. The new test line looks like this:
1 | self.assertEqual(response.session.has_key('address'), True) |
Which provides the more useful failure message:
1 2 3 4 5 6 7 8 | .F ====================================================================== FAIL: test_sess (testexample.tests.functional.test_comments.TestCommentsController) ---------------------------------------------------------------------- Traceback (most recent call last): File "~/TestExample/testexample/tests/functional/test_comments.py", line 12, in test_sess self.assertEqual(response.session.has_key('address'), True) AssertionError: False != True |
Testing Pylons Objects
Pylons will provide several additional attributes for the paste.fixture response object that let you access various objects that were created during the web request:
- session
- Session object
- req
- Request object
- c
- Object containing variables passed to templates
- g
- Globals object
To use them, merely access the attributes of the response after you've used a get/post command:
1 2 3 | response = app.get('/some/url') assert response.session['var'] == 4 assert 'REQUEST_METHOD' in response.req.environ |
Note
The paste.fixture response object already has a TestRequest object assigned to it, therefore Pylons assigns its request object to the response as req.
Testing Your Own Objects
Paste's fixture testing allows you to designate your own objects that you'd like to access in your tests. This powerful functionality makes it easy to test the value of objects that are normally only retained for the duration of a single request.
Before making objects available for testing, its useful to know when your application is being tested. Paste will provide an environ variable called paste.testing that you can test for the presence and truth of so that your application only populates the testing objects when it has to.
Populating the paste.fixture response object with your objects is done by adding them to the environ dict under the key paste.testing_variables. Pylons creates this dict before calling your application, so testing for its existence and adding new values to it is recommended. All variables assigned to the paste.testing_variables dict will be available on the response object with the key being the attribute name.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # testexample/lib/base.py from pylons import c, g, cache, request, response, session from pylons.controllers import WSGIController from pylons.decorators import jsonify, rest, validate from pylons.templating import render, render_response from pylons.helpers import abort, redirect_to, etag_cache import testexample.models as model import testexample.helpers as h class BaseController(WSGIController): def __call__(self, environ, start_response): # Create a custom email object email = MyCustomEmailObj() email.name = 'Fred Smith' if 'paste.testing_variables' in request.environ: request.environ['paste.testing_variables']['email'] = email return WSGIController.__call__(self, environ, start_response) # testexample/tests/functional/test_controller.py from testexample.tests import * class TestCommentsController(TestController): def test_index(self): response = self.app.get(url_for(controller='/')) assert response.email.name == 'Fred Smith' |
More Details
For more details on running tests using get/post, and testing the response, headers, etc., see the Paste document on web application testing:
Comments (8)
Sep 12, 2007
Pekka Jääskeläinen says:
Is it possible to pass Python objects to the tested controller from an unit test...Is it possible to pass Python objects to the tested controller from an unit test? I found only a way to pass strings using the extra_environ or in the request string. Passing objects would make testing certain type of controllers easier.
Oct 25, 2007
darrell maples says:
for testing, here is another way to solve the error caused by importing modules ...for testing, here is another way to solve the error caused by importing modules from the model before the pylons app has properly loaded (described in the warning above):
add something like this to your models (python 2.5 syntax)
and somewhere in your tests after the pylons app has loaded, fetch the SA engine from the pylons config and properly bind the metadata
Nov 13, 2007
Daniel Pronych says:
I've searched through documentation and am unable to locate a guide to implement...I've searched through documentation and am unable to locate a guide to implementing unit testing with Pylons 0.9.6.1, SQAlchemy 0.4.0 and AuthKit 0.4 working together in unison. My entire application is also secured by AuthKit in my middleware so that any web request requires an authenticated user, and authorization for only some routines. I've also had to patch AuthKit's user driver to use the SQLAlchemy 0.4 format and know that it is functioning correctly. It would be nice if there was a good explanation to doing this. Thanks.
Nov 18, 2007
Pavel Skvazh says:
Can anyone plz tip me, how do you specify global variables to be seen inside the...Can anyone plz tip me, how do you specify global variables to be seen inside the test?
For example, I want to write something to a global session object? Or specify an array, that'll be seen inside my test?
Thanks
Feb 03, 2009
Matt Doar says:
Me too!Me too!
Mar 15, 2008
Daniele Paolella says:
If you wish to step into debugger while running your tests, pdb.set_trace() won'...If you wish to step into debugger while running your tests, pdb.set_trace() won't restore stdout by itself; instead you can
to hardcode an usable breakpoint.
Apr 08, 2008
Zachery Bir says:
Daniele: You can also run nose with `--nocapture` to get back your `pdb.set_trac...Daniele: You can also run nose with `--nocapture` to get back your `pdb.set_trace()` functionality.
Apr 09, 2009
George Katsitadze says:
nosetests still needs to be run with '-e model' option. Either that or [nose...nosetests still needs to be run with '-e model' option. Either that or [nosetests] section in setup.cfg should have the 'where=./appname/tests' line (as mentioned here: http://pylonshq.com/project/pylonshq/ticket/287).