tdd
Oct 26, 2007
Debugging Functional Browser tests
I've just had one of those slap-yourself-on-the-forehead-why-haven't-I-thought-of-this-before moments that I'd like to share with my esteemed readership.
In Martin Aspeli's new (and highly recommended) book Professional Plone Development he shares a handy little tidbit regarding the debugging of the raw console output of a browser test's page contents:
"Because reading raw HTML in the console can be a little cumbersome, the author sometimes uses this trick during debugging:
>>> open('/tmp/test-output.html', 'w').write(browser.contents)
[...] When executed, this statement will write the current contents of the test-browser to
/tmp/test-output.html. Images and stylesheets will not work, but the text in the page is normally enough to understand why some output is not as expected."
That's a real life-saver right there. But it can still be a bit cumbersome if you're debugging a form interactively. So I've done the following:
-
Subclass Five's
Browserclass - add a convenience method for dumping the contents
- in tests import the extended browser class
In a nutshell this is what I added to my current project's base.py test class:
from Products.Five.testbrowser import Browser as BaseBrowser
[...]
class Browser(BaseBrowser):
def dc(self, filename="/tmp/test-output.html"):
open(filename, 'w').write(self.contents)
In my tests I use the abovementioned browser instead of Five's and whenever I'm in a debug session, I just enter browser.dc() to get a 'snapshot' of the current browser page. The tmp-file is left open in my editor (TextMate, which does an excellent job of picking up refreshed contents)...
Debugging a form with a large number of fields, spread out over multiple pages has just become a whole lot easier ;-)
Sep 20, 2007
Debugging browser tests in Plone 3.0
Some things were better back in the old days...
Naturally, this makes debugging page templates using browser tests more tedious (than it already is). Solution? Use the verbose error message template from Plone 2.5. Like so:
svn cat https://svn.plone.org/svn/plone/CMFPlone/tags/2.5.3/skins/plone_templates/default_error_message.pt > parts/plone/CMFPlone/skins/plone_templates/default_error_message.pt
Now you can actually see which attribute caused an AttributeError etc. Usually, that helps ;-)
Aug 24, 2007
Integrating pure Python tests with PloneTestCase
Running tests takes long enough as it is...
Often, when writing a Plone Product I come across small methods that I need to implement, that actually have no dependency upon Plone itself but are too trivial and/or specific as to warrant putting them into a separate package. If that's the case for you, too, you will undoubtedly start to get impatient when testing these methods, because with each friggin' testrun an entire zope instance is created and torn down again. This I have become used to and accept it as part of life as Zope developer, if the code I'm testing actually needs that setup. But it just kept bugging me that I need to wait so long for tests that actually execute in under one second. So I've poked around today and came up with the following scenario:
- put your methods into a separate module that has no import dependencies from zope
- add a test runner to that module to execute its tests without starting up zope
- create a test suite for your product that will pick up the tests of your module
1. standalone module
For example, I've created a (crude) helper for batching some items in a file named batching.py on the root level of my Product (along with a little doctest testing some arbitrary edge cases):def batch(items=[], batch=0, batchsize=5):I can use this method in my product with the following import statement:
"""returns a batch from the given items
>>> items = range(12)
>>> batch(items, 3, 5)
Traceback (most recent call last):
...
IndexError: No batch number 3
>>> batch(items, 0, 5)
[0, 1, 2, 3, 4]
>>> batch(items, 1, 5)
[5, 6, 7, 8, 9]
>>> batch(items, 2, 5)
[10, 11]
>>> batch(items, 2, 4)
[8, 9, 10, 11]
"""
if batch >= batchnum(len(items), batchsize):
raise IndexError, \
"No batch number %d" % batch
else:
return items[batch * batchsize:(batch+1) * batchsize]
from Products.PRODUCTNAME.batching import batch
2. add a test runner
Here you just need to follow the straightforward example given in the doctest documentation and add the following snippet to the bottom of batching.py:if __name__ == "__main__":Now you can simply execute your module and it will run its tests at blazing speed - buckle up, Dorothy... This definitely makes for much nicer test driven development!
import doctest
doctest.testmod()
3. Zope integration
Now we just need to make sure, that the tests will also be executed, when we run our Product's entire suite. This is done by writing a light-weight Testcase based on Zope's DocTestSuite that 'picks' up the standalone tests. I've named mine 'testUtilities.py' and stuck it right into my Product's 'test' folder:import unittestEdit: removed some superflous code in the examples.
from unittest import TestSuite
from zope.testing.doctestunit import DocTestSuite
def test_suite():
return TestSuite((
DocTestSuite('Products.PRODUCTNAME.batching'),
))
if __name__ == '__main__':
unittest.main(defaultTest="test_suite")
