Migrating Tests to TurboGears 1.1

Background

The TurboGears 1.0 testing system has been a limiting factor in moving TurboGears forward [1]. Regardless of whether the “future” involves CherryPy 3.0, Pylons, or alien technology (read: Google AppEngine), the dependencies of testutil.create_request, etc. on the internals of CherryPy 2.0 restricts TurboGears from using the features of these newer systems.

The core TurboGears developers identified this problem at least as early as May, 2007 [2], and have agreed that a framework agnostic testing tool would provide a solution that is in line with TurboGears’ philosophy. Ian Bicking’s WebTest has been selected as the best tool for this purpose, and is being introduced in the TG 1.1 series.

In order to support this move, several functions in testutil have been deprecated in favor of the the new methodology. You can continue to run your tests unchanged for now, but you will receive DeprecationWarnings. The deprecated features will be removed in the next version of TurboGears, 1.5.

Note

Earlier versions of this document emphasized the use of mount & make_app over the use of TGTest. In response to feedback, this has been reversed in the current version. TGTest now receives primary attention and mount & make_app are reserved for discussion in the “Other Testing Approaches” of the main Testing document. This is purely a matter of emphasis; the approach detailed in the old doc is still documented & supported.

Introducing TGTest

testutil.TGTest is a unittest.TestCase-style class that makes it easy to setup your TurboGears tests. Simply subclass it and assign your RootController to the root attribute of your subclass. Your test methods will have access to a WebTest.TestApp instance as self.app. We’ll describe WebTest in more detail below, but for now let’s look at a simple test:

class TestIt(turbogears.testutil.TGTest):

   root = myproject.controllers.Root

   def test_index(self):
       response = self.app.get('/index')
       assert 'Index this' in response

The main thing to note for people upgrading from TG 1.0 is that instead of mounting our application by assigning our root controller to cherrypy.root, we assign it to a root attribute of our test class.

TGTest will do the work of starting the server, mounting your application, creating a WebTest.TestApp instance and assigning it to self.app to make it available to you in your testing. If you need finer-grained control of how your tests are set up, you can find it covered in the “Other Testing Approaches” of the main Testing document.

Working with WebTest

So let’s get back to that code segment. Once we’ve set up our testutil.TGTest subclass, we’re ready to start using the features of WebTest. As covered above, the self.app attribute is a WebTest.TestApp instance, set up & ready to test the application. This instance has a method for every HTTP verb, each of which returns a response object. You need to use this response in your tests instead of cherrypy.response [3]. For example, the TG 1.0 test:

cherrypy.root = MyRoot()
testutil.create_request('/mysite')
assert 'is groovy' in cherrypy.response.body[0]

becomes this in TG 1.1:

response = self.app.get('/mysite')
assert 'is groovy' in response

In addition to WebTest’s normal attributes, this response also contains your controller’s return value under the raw attribute. This features allows app.get() to be the replacement not only for testutil.create_request, but also testutil.call, and testutil.call_with_request as well.

TurboGears 1.0:

d = testutil.call(cherrypy.root.foo)
assert d["title"] == "Foobar"

Becomes this in >= 1.1:

d = self.app.get("/foo")
assert d.raw['title'] == "Foobar"

Finally, WebTest will automatically check the status code for your application. By default, any status code other than 2XX or 3XX will be a test failure. You can override this behavior by specifying the status code that you expect on the status parameter. Pass '*' to accept any status.

Testing Secured Resources

testutil.set_identity_user and testutil.attach_identity have been deprecated. Instead of manipulating the identity system behind the scenes, it’s recommended that your tests log in to your application the same way they would in a live environment:

def test_secure_access(self):
     response = self.app.get('/secured?'
         'user_name=frodo&password=secret&login=Login')
     assert 'Logged in' in response

     session_id = response.headers['Set-Cookie']
     response = self.app.get('/logout', headers={'Cookie': session_id })
     assert response.body == 'Logged out' in response

If you absolutely need to fake the login process, you should be able to do so by passing REMOTE_USER into the extra_environ dictionary:

self.app.get('/secret', extra_environ=dict(REMOTE_USER='bob'))

But that’s untested, so your mileage may vary.

POST And Such

What if you want to do more than just a HTTP “get”? This wasn’t possible using create_request, but is simple as pie using WebTest. Let’s check out a simple form example first:

response = self.app.get('/mysite')
response.form['foo'] = 1
response.form.submit()

Put and delete are similarly easy:

self.app.put('/mysite/1', params)
self.app.delete('/mysite/1')

Further Reading

This has been a quick, but complete, run-through of all of the new & deprecated features of the testutil module. The main Testing document covers this same information, with more background information for people new to testing with TurboGears and without the discussion of deprecated functionality. The docs for WebTest are also good, so be sure to read those if you have any questions.

Errata

The items below are expected to only impact the internal TurboGears tests; they shouldn’t impact tests of applications based on TurboGears. They’re only included here for the sake of completeness.

  • Under the old create_request system you could get away with returning None from your test controllers. Using WebTest, you’ll need to return a valid TG value, such as a string, list, dictionary, or equivalent.
  • Tests that involve requests, or request-based features such as view.render, need to be checked to make sure they’re actually done inside of a request – unlike the TG 1.0 tests, requests in the TG 1.1 tests are short-lived & can’t be assumed to be available after the controller has returned a value. Again, see [3]
[1]See Testing form submittal (2007-12-29) ANN: TurboGears 1.0.4.2 Released (2008-01-21) and 1.0 and 1.1 testing (2008-03-18)
[2]See http://www.cherrypy.org/wiki/TGIRC20070526, http://groups.google.com/group/turbogears/browse_thread/thread/6eca2f550b13a8a5
[3](1, 2) If you try to use cherrypy.request after using WebTest to hit your page, you’ll get an error:: AttributeError: cherrypy.request has no properties outside of an HTTP request.