Feed on
Posts
Comments

I remember that day about a month ago, when it occured to me it is high time that I write some unit tests for my project. In the beginning, it felt like travelling where no man has gone before - a lot of technical issues to settle. I introduced my BrowserSession class to aid stateful unit testing few days ago. From responses that I got, it seems that a lot of people are still fighting with the technicalities of setting up the testing environment. So I took the time to give some hints and tips on the matter. Attached to this post is a project which demonstrates simple identity tests (that work).

Before you start, it will be helpful (but absolutely not required) if you also read about the BrowserSession object that is used in this post.

Tip 1: Set up the test configuration right.

None of your configuration files (dev.cfg, prod.cfg or app.cfg) is not loaded when you run your tests. Therefore, at the beginning of test_controllers.py, set your configuration options:

turbogears.config.update({
    'visit.on': True,
    'identity.on': True,
    'identity.failure_url': '/login',
    })

Another possibility is to create a configuration environment for testing, test.cfg and manually load it, using something like:

turbogears.update_config(configfile='project_dir/test.cfg',
                         modulename='package_name.config')

Tip 2: Initialize your database right.

testutil will set the default database to in-memory sqlite database. This looks like a good choice to me (you want something that will not be affected by previous runs and is fast). If you want to change that, make sure you do it after you import testutil. I never tried this.
In any case, your tables may not exist, and it is a good time to create them once the test module is imported. You can also use this opportunity to fill the tables with some data you want to use in your tests later:

def init_database():
    # hack follows: to create a TG_User, a request
    # environment must be around...
    testutil.create_request('/')
    if TG_User.selectBy(user_name='thesamet').count() == 0:
        TG_User(user_name='thesamet', password='password',
                display_name='Nadav', email_address='spam@me.not')

    Table.create(ifNotExists=True)
    Table.create(table_number=3, table_location='far away')

Note the comment at the beginning of the function. Unless there is a cherrypy request around, you can’t create a TG_User record. This is due to some unfortunate coupling.

Tip 3: Identity likes your login buttons.

When you emulate a user filling the login form, don’t forget to pass also the login button value (which is ‘Login’). If you login through the /login url (as opposed to give a url of some identity-protected method), do not forget to pass the forward_url argument. Example:

user = BrowserSession()
user.goto('/login?user_name=thesamet&password=password' \\
          '&login=Login&forward_url=/')
assert user.status != 403     # not forbidden

Tip 4: Never forget to call stopTurboGears()

You must call turbogears.startup.stopTurboGears() at the end of each test. I won’t pretend that I understand why it is needed. It is also done in TurboGears’ own identity tests. If you don’t do that you’ll get nice exceptions from the VisitManager thread. I like to factor out the stopTurboGears() call to the tearDown part of my test case.

The following is an example of a test case objects that verifies that the protected resource /secret is well-protected:

class SecretPageTest(unittest.TestCase):
    def setUp(self):
        self.user = BrowserSession()

    def test_anonymous(self):
        self.user.goto('/')
        assert 'Login' in self.user.response
        self.user.goto('/secret')
        assert 'You must provide your credentials' \\
                  in self.user.response

    def test_bad_credentials(self):
        self.user.goto('/secret?user_name=thesamet&password=incorrect'\\
                       '&login=Login')
        assert 'The credentials you supplied were not correct' \\
                           in self.user.response

    def test_successful_login(self):
        self.user.goto('/secret?user_name=thesamet&password=password' \\
                       '&login=Login')
        assert 'This is a secret page' in self.user.response

    def test_logout(self):
        self.user.goto('/login?user_name=thesamet&password=password' \\
                       '&login=Login&forward_url=/')
        # not forbidden
        assert self.user.status != 403
        self.user.goto('/')
        # display name should appear and no suggestion to login
        assert 'Nadav' in self.user.response
        assert '<A HREF="/login">Login</A>' not in self.user.response
        self.user.goto('/logout')
        self.user.goto('/')
        # no display name should appear and a suggestion to login
        assert 'Nadav' not in self.user.response
        assert '<A HREF="/login">Login</A>' in self.user.response

    def tearDown(self):
        turbogears.startup.stopTurboGears()

You can download the full source code of a minimal identity testable project.

RSS feed | Trackback URI

5 Comments »

Comment by Arthur Clune
2006-06-19 03:24:32

Many thanks for these tips - I’ve now got it all working nicely.

Cheers!

Arthur

 
Comment by Peter Russell
2006-07-22 12:24:45

It’s worth noting that if you have a quickstarted project then you will probably have classes in your model called User, Group and Permission, rather than the TG_User, TG_Group and TG_Permission that is used by default in turbogears.identity. You therefore have to set those correctly in your test configuration. Look at config/app.cfg to see what those values are.

 
Comment by Josh Subscribed to comments via email
2007-06-05 07:03:13

Hmm.. any reason turbogears.startup.stopTurbogears() is commented out in the downloaded source’s tearDown()?

 
Comment by thesamet
2007-06-05 07:29:10

Hi Josh,

Can’t remember at the moment why… As this was posted a while ago, it might have been an issue with tg which is already fixed.

 
Comment by Jamie Subscribed to comments via email
2007-09-19 12:08:38

I’m very new to Turbogears. I downloaded your “full source code of a minimal identity testable project.”

from the WindowsXP commandline I ran:
python start-idtest.py
which loads up the webserver to port 8080 (just like all the other tutorials etc I’ve run).

I thought I’d view the site in a browser (IE 6.0) by going to the following URL: http://localhost:8080/

I’m shown an ERROR page:
—————————————–
500 Internal error
The server encountered an unexpected condition which prevented it from fulfilling the request.

Traceback (most recent call last):
File “c:\python25\lib\site-packages\cherrypy-2.2.1-py2.5.egg\cherrypy\_cphttptools.py”, line 103, in _run
applyFilters(’before_main’)
File “c:\python25\lib\site-packages\cherrypy-2.2.1-py2.5.egg\cherrypy\filters\__init__.py”, line 151, in applyFilters
method()
File “c:\python25\lib\site-packages\TurboGears-1.0.3.2-py2.5.egg\turbogears\visit\api.py”, line 146, in before_main
visit = _manager.new_visit_with_key(visit_key)
File “c:\python25\lib\site-packages\TurboGears-1.0.3.2-py2.5.egg\turbogears\visit\sovisit.py”, line 44, in new_visit_with_key
visit= visit_class( visit_key=visit_key, expiry=datetime.now()+self.timeout )
File “c:\python25\lib\site-packages\SQLObject-0.9.1-py2.5.egg\sqlobject\declarative.py”, line 98, in _wrapper
return fn(self, *args, **kwargs)
File “c:\python25\lib\site-packages\SQLObject-0.9.1-py2.5.egg\sqlobject\main.py”, line 1218, in __init__
self._create(id, **kw)
File “c:\python25\lib\site-packages\SQLObject-0.9.1-py2.5.egg\sqlobject\main.py”, line 1246, in _create
self.set(**kw)
File “c:\python25\lib\site-packages\SQLObject-0.9.1-py2.5.egg\sqlobject\main.py”, line 1093, in set
kw[name] = dbValue = from_python(value, self._SO_validatorState)
File “c:\python25\lib\site-packages\SQLObject-0.9.1-py2.5.egg\sqlobject\col.py”, line 596, in from_python
(self.name, type(value), value), value, state)
Invalid: expected an int in the IntCol ‘user_id’, got instead

—————————————–

I’ve also run ‘nosetests’ from the root folder of the project and 3 of the 4 tests fail. I decided to print out self.user.response and it mentions the INTERNAL 500 ERROR in the response. Here is the body text:
—————————————–
500 Internal error
The server encountered an unexpected condition which prevented it fro
m fulfilling the request.
Traceback (most recent call last):
File “c:\python25\lib\site-packages\cherrypy-2.2.1-py2.5.egg\cherrypy\_cphttpt
ools.py”, line 103, in _run
applyFilters(’before_main’)
File “c:\python25\lib\site-packages\cherrypy-2.2.1-py2.5.egg\cherrypy\filters\
__init__.py”, line 151, in applyFilters
method()
File “c:\python25\lib\site-packages\TurboGears-1.0.3.2-py2.5.egg\turbogears\vi
sit\api.py”, line 146, in before_main
visit = _manager.new_visit_with_key(visit_key)
AttributeError: ‘NoneType’ object has no attribute ‘new_visit_with_key’

Powered by CherryPy 2.2.1

————————————–

I’m running Python 2.5
SQL:Lite
all defaults as far as I know, I just used easy_install etc.

 
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.