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!
Pingback: Compound Thinking » TurboGears Identity testing
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
Yes, I was saying like some adsmrtiveeent banner which is just fixed on any side of the blog’s layout, when page rolls down, banner remains there, just like chatting snippet in facebook, it remains fix on right side. I was saying, post the coding and tutorial on this topic.