Latest Version: 0.9.6.1
  Dashboard > Pylons Official Docs > Home > Unit Testing
  Pylons Official Docs Log In | Sign Up   View a printable version of the current page.  
  Unit Testing
Added by James Gardner, last edited by Ches Martin on Oct 12, 2007  (view change) show comment
Labels: 
(None)

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:

Testing Application with Paste, the Tests

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.

Posted by Pekka Jääskeläinen at Sep 12, 2007 08:40 | Permalink

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)

    engine = config['pylons.g'].sa_engine if config['pylons.g'] else None
    Session = scoped_session(sessionmaker(autoflush=True, transactional=True, bind=engine)) 

and somewhere in your tests after the pylons app has loaded, fetch the SA engine from the pylons config and properly bind the metadata

    models.metadata.bind = config['pylons.g'].sa_engine


Posted by darrell maples at Oct 25, 2007 16:47 | Permalink

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.

Posted by Daniel Pronych at Nov 13, 2007 19:30 | Permalink

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

Posted by Pavel Skvazh at Nov 18, 2007 20:16 | Permalink

If you wish to step into debugger while running your tests, pdb.set_trace() won't restore stdout by itself; instead you can

1
import nose; nose.tools.set_trace()

to hardcode an usable breakpoint.

Posted by Daniele Paolella at Mar 15, 2008 13:11 | Permalink

Daniele: You can also run nose with `--nocapture` to get back your `pdb.set_trace()` functionality.

Posted by Zachery Bir at Apr 08, 2008 20:28 | Permalink
Site running on a free Atlassian Confluence Open Source Project License granted to Pylons. Evaluate Confluence today.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.3.3 Build:#645 Feb 13, 2007) - Bug/feature request - Contact Administrators
Top