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.
Many thanks for these tips – I’ve now got it all working nicely.
Cheers!
Arthur
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.
Hmm.. any reason turbogears.startup.stopTurbogears() is commented out in the downloaded source’s tearDown()?
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.
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.