Testing Multi-user TurboGears Applications

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!

This entry was posted in turbogears. Bookmark the permalink.

7 Responses to Testing Multi-user TurboGears Applications

  1. Pingback: Compound Thinking » TurboGears Identity testing

  2. 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!

  3. Max says:

    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).

  4. Arthur Clune says:

    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

  5. thesamet says:

    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)

  6. jorge.vargas says:

    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

  7. Adam says:

    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.

Leave a Reply

Your email address will not be published. Required fields are marked *