Testing Multi-user TurboGears Applications
May 31st, 2006 by thesamet
Lately I’ve started to write many unit tests for my secret turbogears project. My secret project, like many other web 2.0 applications, can have a lot of users logged in at the same time and the actions of one user can affect other users.
The current testing tools provided by TurboGears do not give “one obvious way” to test multi-user scenarios. I came up last week with a simple class that will emulate a browser session of a single user against a cherrypy server. Its only task is actually to keep track of the visit cookie. This is a simple example of how I use it:
def test_breaking_in():
user = BrowserSession()
user.goto('/directory/for/members')
assert 'You must login' in user.response
assert '403' in user.status
user.goto('/login?username=thesamet&password=secret')
user.goto('/directory/for/members')
assert 'Welcome to the members area' in user.response
The BrowserSession object truly shines when your testcase includes two users (or more):
def test_object_lock():
user1 = BrowserSession()
user2 = BrowserSession()
user1.goto('/login?username=user1&password=pass1')
user2.goto('/login?username=user2&password=pass2')
user1.goto('/edit?page=pagex')
user2.goto('/edit?page=pagex')
assert 'Page is locked by user1' in user2.response
user1.goto('/edit?page=pagex')
assert "page is locked by user1" not in user1.response
and no - my secret project is not another wiki. I later found it very convenient to put test methods inside a TestCase object rather than many individual test functions. One of the benefits is that you can factor out the repeating code of the logins that appear in many tests to the setUp() method. In each test method, we will have new and fresh sessions:
class TwoUsersTestCase(unittest.TestCase):
def setUp(self):
self.user1 = BrowserSession()
self.user2 = BrowserSession()
self.user1.goto('/login?username=user1&password=pass1')
self.user2.goto('/login?username=user2&password=pass2')
class SendPrivateMessageTestCase(TwoUsersTestCase):
def test_send_message(self):
self.user1.goto('/chat/send?to=user2&text=hello')
self.user2.goto('/chat/receive')
assert 'hello' in self.user2.response
def test_flood_detection(self):
for x in xrange(100):
self.user1.goto('/chat/send?to=user2&text=hello')
self.user2.goto('/chat/receive')
assert 'user1 kicked out (flooding)' in self.user2.response
Now I’ll share with you the BrowserSession code. Feel free to use it:
import cherrypy
from turbogears import testutil
def cookie_header(morsel):
"""Returns a dict containing cookie information to pass to a server."""
return {'Cookie': morsel.output(header="")[1:]}
class BrowserSession(object):
def __init__(self):
self.cookie_name = turbogears.config.get(’visit.cookie.name’, ‘tg-visit’)
self.visit = None
self.response, self.status = None, None
def goto(self, *args, **kwargs):
if self.visit:
headers = kwargs.get(’headers’, {})
headers.update(cookie_header(self.visit))
kwargs['headers'] = headers
testutil.createRequest(*args, **kwargs)
if self.cookie_name in cherrypy.response.simple_cookie:
self.visit = cherrypy.response.simple_cookie[self.cookie_name]
self.response = cherrypy.response.body[0]
self.status = cherrypy.response.status
Ideas and suggestions are welcome!
[...] Or you could just do the simplist thing that could work, like Nadav Samet. [...]
Would you mind submitting this to the track for milestone 1.0b1? You’re right that we don’t have one obvious way to do this, and this looks like a good solution!
It didn’t work for me.
I read through identity source code and noticed that parameters you pass in login url must match the names it expects, plus login button name must be present as well. E.g.:
br.goto(’/login?user_name=anna&password=x&login=Login’)
Nevertheless, it still doesn’t work for some reason so I gave up (for now).
This looks like just what I need, but I can’t get this to work.
The minor point is that you need an
import turbogears
in the BrowserSession code. Also it might be good to document the best way of setting up the cherrypy session when using this code. I had to add
cherrypy.root = Root()
to my test case code, or I got various cherrypy errors.
The more serious problem is that trying to visit a URL when un-authenicated gives me an error page
File “/opt/local/Library/Frameworks/Python.framework/Versions/2.4/lib/python2.4/site-packages/TurboGears-0.9a6-py2.4.egg/turbogears/identity/exceptions.py”, line 64, in __init__
raise IdentityConfigurationException( msg )
IdentityConfigurationException: Missing URL for identity failure
when in my app.cfg file I have
identity.failure_url=”/login”
This could all just be me being foolish of course, so any hints welcome!
Cheers,
Arthur
Arthur, you should set identity.failure_url manually in your testing module.
I’ve added a follow-up article which discuss exactly this kind of issues (see the next post in this blog)
Hey I was looking at http://trac.turbogears.org/turbogears/ticket/930
could you incorporate the comments here and make it a full grown patch to turbogears/testutil.py