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 (mock, Twisted's
trial, 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
available.
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
import os to
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
the _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.