For example, you might want to monkey patch time.time so that it returns repeatable timestamps during the test. We have quite a lot of test cases that do something like this:
class TestFoo(unittest.TestCase): def setUp(self): self._old_time = time.time def monkey_time(): return 0 time.time = monkey_time def tearDown(self): time.time = self._old_time def test_foo(self): # body of test caseHaving to save and restore the old values gets tedious, particularly if you have to monkey patch several objects (and, unfortunately, there are a few tests that monkey patch a lot). So I introduced a monkey_patch() method so that the code above can be simplified to:
class TestFoo(TestCase): def test_foo(self): self.monkey_patch(time, "time", lambda: 0) # body of test case(OK, I'm cheating by using a lambda the second time around to make the code look shorter!)
Now, monkey patching is not ideal, and I would prefer not to have to use it. When I write new code I try to make sure that it can be tested without resorting to monkey patching. So, for example, I would parameterize the software under test to take time.time as an argument instead of getting it directly from the time module. (here's an example).
But sometimes you have to work with a codebase where most of the code is not covered by tests and is structured in such a way that adding tests is difficult. You could refactor the code to be more testable, but that risks changing its behaviour and breaking it. In that situation, monkey patching can be very useful. Once you have some tests, refactoring can become easier and less risky. It is then easier to refactor to remove the need for monkey patching -- although in practice it can be hard to justify doing that, because it is relatively invasive and might not be a big improvement, and so the monkey patching stays in.
Here's the code, an extended version of the base class from the earlier post:
import os import shutil import tempfile import unittest class TestCase(unittest.TestCase): def setUp(self): self._on_teardown = [] def make_temp_dir(self): temp_dir = tempfile.mkdtemp(prefix="tmp-%s-" % self.__class__.__name__) def tear_down(): shutil.rmtree(temp_dir) self._on_teardown.append(tear_down) return temp_dir def monkey_patch(self, obj, attr, new_value): old_value = getattr(obj, attr) def tear_down(): setattr(obj, attr, old_value) self._on_teardown.append(tear_down) setattr(obj, attr, new_value) def monkey_patch_environ(self, key, value): old_value = os.environ.get(key) def tear_down(): if old_value is None: del os.environ[key] else: os.environ[key] = old_value self._on_teardown.append(tear_down) os.environ[key] = value def tearDown(self): for func in reversed(self._on_teardown): func()