Tuesday, June 02, 2009

Testing ZODB changes

Came up with a handy decorator for asserting that a given method does not modify the ZODB.

It can be used in tests to ensure that a given test does not change the ZODB in any way (for example views).

I came up with it because a regression was discovered where a read-only view was modifying some things in ZODB causing ConflictErrors.To ensure it would not come up again I had to come up with a failing test.

def readonly(fun):
  """
    requires that self is at least a PortalTestCase
    
    Usage hint:  Use a savepoint to reset the changed object listing.
    """

  def decorated(self, *args, **kwargs):
      # this resets registered objects
      transaction.savepoint()
      before = list(self.portal._p_jar._registered_objects)
      result = fun(self, *args, **kwargs)
      after = self.portal._p_jar._registered_objects
      if before != after:
          s = SequenceMatcher(a=before, b=after)
          for tag, i1, i2, j1, j2 in s.get_opcodes():
              print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % (tag, i1, i2, before[i1:i2], j1, j2, after[j1:j2]))
          raise RuntimeError("ZODB was changed")
      return result
  return decorated

An example of usage:

class ReadonlyTests(YourTestCase):
 @readonly
 # This one will fail
 def test_isnt_readonly(self):
     self.portal.invokeFactory('Document', 'test')
     self.portal.test.setTitle('foo')

 @readonly
 def test_is_readonly(self):
     pass

No comments: