Feed on
Posts
Comments

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!

RSS feed | Trackback URI

6 Comments »

2006-05-31 20:54:03

[...] Or you could just do the simplist thing that could work, like Nadav Samet. [...]

 
Comment by Kevin Dangoor
2006-05-31 22:03:26

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!

 
Comment by Max
2006-06-01 03:40:57

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

 
Comment by Arthur Clune
2006-06-01 10:50:31

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

 
Comment by thesamet
2006-06-02 09:54:32

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)

 
Comment by jorge.vargas
2006-10-10 21:12:31

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

 
Name (required)
E-mail (required - never shown publicly)
URI
How much is two plus four? (required, to check if you are human)
Subscribe to comments via email
Your Comment (smaller size | larger size)
You may use <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> in your comment.