This post is a collection of python unit testing ‘gotchas’.
Mocking external packages
import sys
import unittest
from unittest.mock import patch, Mock, call, mock_open
from io import StringIO, TextIOWrapper
mock_os = Mock()
sys.modules["os"] = mock_os
from config_editor import ConfigEditor
Now we have effectively mocked all actual file I/O in our tests
Mocking in-built methods and file I/O
Super useful if you want to test a unit that uses the builtin open
method. The following example is a class for modifyinh a system config file, but the unit tests never need touch the file system. The setUp
defines the initial state of the file, and then each test can evaluate its expected and final state.
class ConfigEditorTestCase(unittest.TestCase):
def setUp(self):
# Create the object to test
self.config_editor = ConfigEditor("/test/file.config")
self.mock_input_file = StringIO("# Title\n"
"#turn_on_rocket_launcher=123\n"
"### turn_on_piranha_launcher=123\n"
"\n"
"enable_flamethrower=true\n"
"enable_a_thing=laser_beam\n"
"#enable_a_thing=blaster_power=500\n"
"enable_a_thing=photon_beam\n"
"\n"
"###############################\n"
"# Some random comment\n"
"###############################\n"
"set_this=thing_to=50000\n")
# Create an empty stream for the output file which we can then inspect
self.mock_output_file = StringIO()
# Patch the in-built "open" method, so it returns our dummy streams
self.mock_open = mock_open()
self.mock_open_method = patch("builtins.open", self.mock_open)
self.mock_open.side_effect = [self.mock_input_file, self.mock_output_file]
self.mock_open_method.start()
Patching objects
Python offers a few ways of patching objects so that they can be used for unit testing. Often you see the with patch()
notation to achieve this in a local scope:
with patch('module.Foo') as mock:
instance = mock.return_value
instance.method.return_value = 'the result'
result = some_function()
assert result == 'the result'
Or sometimes in the form of a method decorator:
@mock.patch("os.listdir", mock.MagicMock(return_value="some_directory_name"))
def test1():
assert "some_directory_name" == os.listdir()
But sometimes you want to patch an object for whole fixture without decorating every test in the same way. Here’s one way to achieve that:
def setUp(self):
# Patch the close method of the two stream, so we can check they are closed correctly
self.mock_close_input_stream = Mock()
self.patch_input_stream_close_method = patch.object(self.mock_input_file, "close", self.mock_close_input_stream)
self.patch_input_stream_close_method.start()
self.mock_close_output_stream = Mock()
self.patch_output_stream_close_method = patch.object(self.mock_output_file, "close", self.mock_close_output_stream)
self.patch_output_stream_close_method.start()
def tearDown(self):
self.mock_open_method.stop()
self.patch_input_stream_close_method.stop()
Making assertions in the tearDown
When you’ve written a few tests which duplicate an assertion, it can be cleaner to put this in the tearDown. This can be especially useful for stuff like resource de-allocation, or ensuring that files handles are closed etc.
def tearDown(self):
# Test that the config files are always opened as expected
self.mock_open.assert_has_calls([call("/test/file.config", "r"), call("/test/file.config.new", "w")])
# Test the config files are always closed
self.mock_close_input_stream.assert_called_once()
self.mock_close_output_stream.assert_called_once()
# Test that the temp file is always cleaned up
mock_os.remove.assert_called_with("/test/file.config.new")