############################################################################## ################################ awmstest.py ################################# ############################################################################## ## ## o author: Alexander Schmolck (a.schmolck@gmx.net) ## o created: 2004-10-09 00:52:25+00:40 ## o last modified: $Date$ ## o keywords: unit testing; python; interactive development ## o license: MIT """ awmstest: a non-painful (i.e. interactive) way to unit-testing and debugging ============================================================================ Suppose you have a module called ``spam_module.py``, and want to develop, debug and test it with unit-testing. The traditional edit-run(everything again from scratch)-debug-cycle is a pain: for example the tests will always be run in the same order, although after making changes to ``spam_module`` you'd like to run the first test that failed last time again at the beginning, to see whether your changes corrected the problem rather than wait for half an hour till all the tests before have finished. Additionally it's often difficult or inconvenient to find the exact causes of a test-failure by just looking at the the test-results print-out, i.e. without the ability to interactively inspect with pdb what has been going on in the frame where the test failed. Wouldn't it be much nicer if you could just develop both ``spam_module.py`` and the corresponding tests in a genuinely interactive fashion (automatically starting a pdb session and jumping to the line in spam_module that failed; starting off again at the same place after making changes to ``spam_module`` etc)? Here ipython/emacs/awmstest come to the rescue. Here is a template how-to: # test_spam_module.py import unittest TestCase = unittest.TestCase TestSuite = unittest.TestSuite # comment out below to get 'normal' behavior; note that even uncommented # there's no real dependency on awmstest; i.e. unittest is used as # fallback try: import awmstest # exceptions are not intercepted -> we end up in debugger after # test-failure TestCase = awmstest.PermeableTestCase # the first failing test is always executed again first TestSuite = awmstest.RotatingTestSuite except ImportError: pass import spam_module class BarTest(TestCase): def testBarness(self): assert spam_module.barness(spam_module.bar) assert "eggs" in spam_module [...] [...] def suite(): makeSuite = lambda *a,**k: unittest.makeSuite(suiteClass=TestSuite,*a,**k) return TestSuite(map(makeSuite, [FooTest, BarTest, QuuxTest, ])) try: theSuite print "Resuming testing were we left off..." except NameError: theSuite = suite() To use interactively in ipython/emacs:: In [1]: @pdb on In [2]: import test_spam_module This runs the tests, till a failure occurs (say in ``BarTest.testBarness``); the failure will throw you into the debugger were you than can inspect what has gone wrong and how (emacs will jump to the right file and line number). Edit and fix ``spam_module``; then:: In [3]: reload(spam_module) and run the test again:: In [4]: reload(test_spam_module) Resuming testing were we left off... (Note: if the first import failed, you might need one more ``import test_spam_module`` before being able to reload) Note that ``BarTest`` is run again *first* although it is second in the list; that's because ``theSuite`` starts again were the first failure occurred (great time-saver for larger ``TestSuite``s). If you need to fix a test in `test_spam_module`, don't forget to In [4]: del test_spam_module.theSuite to make sure you start again from scratch, with a new suite on:: In [5]: reload(test_spam_module) """ __docformat__ = "restructuredtext en" __revision__ = "$Id$" __version__ = "0.1" __author__ = "Alexander Schmolck (A.Schmolck@gmx.net)" __all__ = ['PermeableTestCase', 'PermeableTestCase2', 'RotatingTestSuite'] import unittest try: from awmstools import iprotate except ImportError: def iprotate(l, steps=1): if len(l): steps %= len(l) if steps: firstPart = l[:steps] del l[:steps] l.extend(firstPart) return l class PermeableTestCase(unittest.TestCase, object): """Like `unittest.TestCase` only that it doesn't suppress Exceptions. This makes it a very useful replacement for `unittest.TestCase` while *developing* unit tests, because it allows easy ``pdb.postmortems`` or (i)python/emacs pdb tracking. In other words, if you got a properly setup ipython/emacs environment (and typed '@pdb on' in ipython) test errors or exceptions will automatically land you at exact source location where the problem occurred, and you can easily navigate the stack trace and introspect the live objects of the test to see what went wrong. This can noticeably speed up the development and debugging of your unit tests. Usage (see module docstring for better example) import unittest, awmstest # just toggle the two by commenting/uncommenting # TestCase = unittest.TestCase TestCase = awmstest.TestCase [...] class ATestCase(TestCase): [...] Note: I notice that with `unittest` the pdb linenumbers somehow seem to get corrupted once you end up in pdb, (so that e.g. 'l' will print the wrong source code), but this shouldn't cause to much of a problem (since the original, printed out stack-trace is correct). """ def __call__(self, result=None): if result is None: result = self.defaultTestResult() result.startTest(self) testMethod = getattr(self, self._TestCase__testMethodName) try: try: self.setUp() testMethod() result.addSuccess(self) finally: self.tearDown() finally: result.stopTest(self) class PermeableTestCase2(unittest.TestCase, object): """A variation on `PermeableTestCase`. Use the source.""" def __call__(self, result=None): if result is None: result = self.defaultTestResult() result.startTest(self) testMethod = getattr(self, self._TestCase__testMethodName) self.setUp() testMethod() result.addSuccess(self) self.tearDown() result.stopTest(self) ## def __call__(self, result): ## for test in self._tests: ## if result.shouldStop: ## break ## test(result) ## return result class RotatingTestSuite(unittest.TestSuite): """Like a normal TestSuite but successful tests are rotated to the end. This is useful for running the same test suite repeatedly in an interactive environment, ironing out bug after bug, because then the first test that failed is the first test to run again. """ def __call__(self, result): for test in self._tests[:]: if result.shouldStop: break test(result) if result.wasSuccessful(): iprotate(self._tests) return result