← Back to team overview

testtools-dev team mailing list archive

[Merge] lp:~jml/testtools/patch-310770 into lp:testtools

 

Jonathan Lange has proposed merging lp:~jml/testtools/patch-310770 into lp:testtools.

Requested reviews:
  testtools developers (testtools-dev)


More and more I've found myself wanting to monkey-patch code with testtools in the same way I can for Trial. This branch adds support for just such a thing.
-- 
https://code.launchpad.net/~jml/testtools/patch-310770/+merge/31666
Your team testtools developers is requested to review the proposed merge of lp:~jml/testtools/patch-310770 into lp:testtools.
=== modified file 'LICENSE'
--- LICENSE	2010-06-10 22:13:14 +0000
+++ LICENSE	2010-08-03 18:09:43 +0000
@@ -1,4 +1,5 @@
-Copyright (c) 2008 Jonathan M. Lange <jml@xxxxxxxxx> and the testtools authors.
+Copyright (c) 2008-2010 Jonathan M. Lange <jml@xxxxxxxxx> and the testtools
+authors.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

=== modified file 'MANUAL'
--- MANUAL	2010-06-15 02:58:47 +0000
+++ MANUAL	2010-08-03 18:09:43 +0000
@@ -52,6 +52,24 @@
 more data (via the addDetails API) and potentially other uses.
 
 
+TestCase.patch
+~~~~~~~~~~~~~~
+
+``patch`` is a convenient way to monkey-patch a Python object for the duration
+of your test.  It's especially useful for testing legacy code.  e.g.::
+
+    def test_foo(self):
+        my_stream = StringIO()
+        self.patch(sys, 'stderr', my_stream)
+        run_some_code_that_prints_to_stderr()
+        self.assertEqual('', my_stream.getvalue())
+
+The call to ``patch`` above masks sys.stderr with 'my_stream' so that anything
+printed to stderr will be captured in a StringIO variable that can be actually
+tested. Once the test is done, the real sys.stderr is restored to its rightful
+place.
+
+
 TestCase.skipTest
 ~~~~~~~~~~~~~~~~~
 

=== modified file 'testtools/testcase.py'
--- testtools/testcase.py	2010-07-29 18:20:02 +0000
+++ testtools/testcase.py	2010-08-03 18:09:43 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2008, 2009 Jonathan M. Lange. See LICENSE for details.
+# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details.
 
 """Test case related stuff."""
 
@@ -121,6 +121,19 @@
         """
         return self.__details
 
+    def patch(self, obj, attribute, value):
+        """Monkey-patch 'obj.attribute' to 'value' while the test is running.
+
+        :param obj: The object to patch. Can be anything.
+        :param attribute: The attribute on 'obj' to patch.
+        :param value: The value to set 'obj.attribute' to.
+        :return: The current value of 'obj.attribute'.
+        """
+        current = getattr(obj, attribute)
+        setattr(obj, attribute, value)
+        self.addCleanup(setattr, obj, attribute, current)
+        return current
+
     def shortDescription(self):
         return self.id()
 
@@ -137,7 +150,7 @@
         """
         raise self.skipException(reason)
 
-    # skipTest is how python2.7 spells this. Sometime in the future 
+    # skipTest is how python2.7 spells this. Sometime in the future
     # This should be given a deprecation decorator - RBC 20100611.
     skip = skipTest
 
@@ -444,7 +457,7 @@
 
 def clone_test_with_new_id(test, new_id):
     """Copy a TestCase, and give the copied test a new id.
-    
+
     This is only expected to be used on tests that have been constructed but
     not executed.
     """

=== modified file 'testtools/tests/test_testtools.py'
--- testtools/tests/test_testtools.py	2010-07-29 12:20:37 +0000
+++ testtools/tests/test_testtools.py	2010-08-03 18:09:43 +0000
@@ -829,6 +829,56 @@
         self.assertThat(events, Equals([]))
 
 
+class TestPatchSupport(TestCase):
+
+    class Case(TestCase):
+        def test(self):
+            pass
+
+    def test_patch(self):
+        # TestCase.patch masks obj.attribute with the new value.
+        self.foo = 'original'
+        test = self.Case('test')
+        test.patch(self, 'foo', 'patched')
+        self.assertEqual('patched', self.foo)
+
+    def test_patch_restored_after_run(self):
+        # TestCase.patch masks obj.attribute with the new value, but restores
+        # the original value after the test is finished.
+        self.foo = 'original'
+        test = self.Case('test')
+        test.patch(self, 'foo', 'patched')
+        test.run()
+        self.assertEqual('original', self.foo)
+
+    def test_patch_returns_original(self):
+        # TestCase.patch returns the current value of the attribute being
+        # patched, just in case you want to do something with it.
+        self.foo = 'original'
+        test = self.Case('test')
+        result = test.patch(self, 'foo', 'patched')
+        self.assertEqual('original', result)
+
+    def test_successive_patches_apply(self):
+        # TestCase.patch can be called multiple times per test. Each time you
+        # call it, it overrides the original value.
+        self.foo = 'original'
+        test = self.Case('test')
+        test.patch(self, 'foo', 'patched')
+        test.patch(self, 'foo', 'second')
+        self.assertEqual('second', self.foo)
+
+    def test_successive_patches_restored_after_run(self):
+        # TestCase.patch restores the original value, no matter how many times
+        # it was called.
+        self.foo = 'original'
+        test = self.Case('test')
+        test.patch(self, 'foo', 'patched')
+        test.patch(self, 'foo', 'second')
+        test.run()
+        self.assertEqual('original', self.foo)
+
+
 def test_suite():
     from unittest import TestLoader
     return TestLoader().loadTestsFromName(__name__)


Follow ups