Python has the power to override any attribute on any module or class, but
just because you can doesn't mean you should. This is true in regular code,
but just as true of unittests. Many testing libraries (
py.test) provide facilities for overriding some piece of global
state; you can also do so manually. Occasionally these facilities prove
invaluable, but often they are used unnecessarily. Better alternatives are
Before I explain why patching is problematic, let's look at an example. Consider the following module:
import os def exit_with_result(function): result = function() if result: os._exit(0) else: os._exit(1)
On the face of it patching is necessary to test this example. The tests would look something like this:
import unittest import os from exitersketch import exit_with_result class ExiterTests(unittest.TestCase): def setUp(self): self.exited = None self.originalExit = os._exit os._exit = self.fakeExit def fakeExit(self, code=0): self.exited = code def tearDown(self): os._exit = self.originalExit def test_exiter_success(self): exit_with_result(lambda: True) self.assertEqual(self.exited, 0) def test_exiter_failure(self): exit_with_result(lambda: False) self.assertEqual(self.exited, 1) if __name__ == '__main__': unittest.main()
Having seen patching, and seen that it works as a testing technique, why should we avoid it?
- Patching is fragile. If the example above changed
from os import _exit, the patching would need to be modified. However, if you forgot to modify the patching, unexpected code will run. In this case, your test run will mysterious exit half way through. If the function you are attempting to patch is more destructive, worse things may happen: credit cards may get charged, data may get deleted, etc..
- Patching leads to unexpected behaviour. Because patching is a global change, the patched code may be called not just by the function being tested, but by code it is calling which happens to use the same patched code.
- Patching indicates bad design. Code code should be designed to be easily testable. Having to modify global state suggests that the code is not as modular as one might hope.
How to avoid patching? Parameterization, aka dependency injection. We refactor the code to accept
_exit function as a parameter. Notice the the public API has not changed:
import os class _API(object): def __init__(self, exit): self.exit = exit def exit_with_result(self, function): result = function() if result: self.exit() else: self.exit(1) _api = _API(os._exit) exit_with_result = _api.exit_with_result
Our tests can now test both that
_API.exit_with_result class has the correct
behavior in general, and that the public
exit_with_result is going to call
os._exit in particular.
import unittest import os from exiter import _api, _API, exit_with_result class ExiterTests(unittest.TestCase): def setUp(self): self.exited = None def fakeExit(self, code=0): self.exited = code def test_api(self): self.assertIsInstance(_api, _API) self.assertEqual(_api.exit, os._exit) self.assertEqual(exit_with_result, _api.exit_with_result) def test_exiter_success(self): _API(self.fakeExit).exit_with_result(lambda: True) self.assertEqual(self.exited, 0) def test_exiter_failure(self): _API(self.fakeExit).exit_with_result(lambda: False) self.assertEqual(self.exited, 1) if __name__ == '__main__': unittest.main()
The same technique is useful when you are tempted to store some state in a module. Instead, store an instance of a class:
class _Counter(object): value = 0 def increment(self): self.value += 1 def value(self): return self.value _counter = _Counter() increment = _counter.increment value = _counter.value
As I've demonstrated, patching can often be avoided by restructuring code to be more testable. The same Python features that make patching so easy also make avoiding patching just as easy. Given the choice, you should avoid changing global state when testing individual components.