The simplest approach is to use mock.mock_open
to simulate reading from a file.
This method works well when one only needs a single return value.
import unittest
from unittest import mock
def core(path):
with open(path, "r") as f:
return f.read()
class TestCore(unittest.TestCase):
@mock.patch("builtins.open", new_callable=mock.mock_open, read_data="foo content")
def test_core_single_file(self, mock_file):
result = core("foo")
self.assertEqual(result, "foo content")
self.assertEqual(mock_file.call_count, 1)
if __name__ == "__main__":
unittest.main()
Limitation: The mock_open
provided by unittest.mock
only supports a single read_data
value, which means it's not suitable for cases where one needs different return values for different files, for example when the accessing 2 files one for reading and another for writing within the same UUT.
Return different content for different files by conditionally returning different mock objects.
import unittest
from unittest import mock
mock_files = {}
def mock_open(path, *args, **kwargs):
if path == "foo.txt":
mock_files[path] = mock.mock_open(read_data="foo content")
elif path == "bar.txt":
mock_files[path] = mock.mock_open(read_data="bar content")
else:
return open(path, *args, **kwargs) # fallback to the real open
return mock_files[path](*args, **kwargs)
def core(path):
with open(path, "r") as f:
return f.read()
class TestCore(unittest.TestCase):
@mock.patch("builtins.open", mock_open)
def test_core_multiple_files(self):
# Test foo.txt
result_foo = core("foo.txt")
self.assertEqual(result_foo, "foo content")
# Test bar.txt
result_bar = core("bar.txt")
self.assertEqual(result_bar, "bar content")
# Check mock_calls
print(mock_files["foo.txt"].mock_calls)
print(mock_files["bar.txt"].mock_calls)
if __name__ == "__main__":
unittest.main()
Pros: Allows different content for different files
Cons: Manual tracking of each mock file object
Encapsulating the management of multiple mocks into a dedicated class allowing different return values for different files.
import unittest
from unittest import mock
class MockOpenManager:
mocks = {}
@staticmethod
def mock_open(path, *args, **kwargs):
if path not in MockOpenManager.mocks:
if path == "foo.txt":
MockOpenManager.mocks[path] = mock.mock_open(read_data="foo content")
elif path == "bar.txt":
MockOpenManager.mocks[path] = mock.mock_open(read_data="bar content")
else:
return open(path, *args, **kwargs) # fallback to the real open
return MockOpenManager.mocks[path](*args, **kwargs)
def core(path):
with open(path, "r") as f:
return f.read()
class TestCore(unittest.TestCase):
@mock.patch("builtins.open", new=MockOpenManager.mock_open)
def test_core_mock_manager(self):
# Test foo.txt
result_foo = core("foo.txt")
self.assertEqual(result_foo, "foo content")
# Test bar.txt
result_bar = core("bar.txt")
self.assertEqual(result_bar, "bar content")
# Validate the mock calls
print("Mock calls for foo.txt:", MockOpenManager.mocks["foo.txt"].mock_calls)
print("Mock calls for bar.txt:", MockOpenManager.mocks["bar.txt"].mock_calls)
if __name__ == "__main__":
unittest.main()
Pros: Mock management is encapsulated in a class. Further extension and modification could be achieved
Cons: Class is required to be modified for extension
Use mock patterns to manage mocks.
import unittest
from unittest import mock
class DynamicMockOpen:
mocks = []
@staticmethod
def add_mock(filename_check, read_data=None, write_data_func=None):
if read_data is not None:
mock_file = mock.mock_open(read_data=read_data)
else:
mock_file = mock.mock_open()
if write_data_func:
mock_file().write.side_effect = write_data_func
DynamicMockOpen.mocks.append((filename_check, mock_file))
@staticmethod
def mock_open(*args, **kwargs):
filepath = args[0]
for check, mock_file in DynamicMockOpen.mocks:
if check(filepath):
return mock_file(*args, **kwargs)
return open(filepath, *args, **kwargs) # Fallback to the real open
@staticmethod
def get_mock_calls(filepath):
for check, mock_file in DynamicMockOpen.mocks:
if check(filepath):
return mock_file.mock_calls
return []
def core(path, mode='r', data=None):
with open(path, mode) as f:
if mode == 'r':
return f.read()
elif mode == 'w' and data:
f.write(data)
return True
return False
class TestCore(unittest.TestCase):
def setUp(self):
self.dynamic_mock = DynamicMockOpen()
self.dynamic_mock.add_mock(
filename_check=lambda filename: filename == "foo.txt",
read_data="foo content"
)
self.dynamic_mock.add_mock(
filename_check=lambda filename: filename == "bar.txt",
write_data_func=lambda data: len(data)
)
dynamic_mock_mgr = DynamicMockOpen
@mock.patch("builtins.open", new=dynamic_mock_mgr.mock_open)
def test_core_dynamic_mock(self):
# Test reading from "foo.txt"
result_foo = core("foo.txt")
self.assertEqual(result_foo, "foo content")
# Test writing to "bar.txt"
result_bar_write = core("bar.txt", mode='w', data="bar content")
self.assertTrue(result_bar_write)
# Validate the mock calls
print("Mock calls for foo.txt:", self.dynamic_mock.get_mock_calls("foo.txt"))
print("Mock calls for bar.txt:", self.dynamic_mock.get_mock_calls("bar.txt"))
if __name__ == "__main__":
unittest.main()
Pros: Extension does not require modifying the class
Cons: Code structure is not OO, only one instance of the class
import unittest
from unittest import mock
class DynamicMockOpen:
def __init__(self):
self.mock_patterns = []
self.mocks = []
def add_mock_pattern(self, filename_check, read_data=None):
if read_data is not None:
mock_file = mock.mock_open(read_data=read_data)
else:
mock_file = mock.mock_open()
self.mock_patterns.append((filename_check, mock_file))
def mock_open(self, *args, **kwargs):
filepath = args[0]
for check, mock_file in self.mock_patterns:
if check(filepath):
out = mock_file(*args, **kwargs)
self.mocks.append(out)
return out
out = open(filepath, *args, **kwargs)
self.mocks.append(out)
return out # Fallback to the real open
def get_mock_calls(self, filepath):
for check, mock_file in self.mock_patterns:
if check(filepath):
return mock_file.mock_calls
return []
@staticmethod
def get_written_data(mock):
CALL_NUM = CALL_DEREF = ARG_NUM = 0
return mock.write.call_args_list[CALL_NUM][CALL_DEREF][ARG_NUM]
def core(path, mode='r', data=None):
with open(path, mode) as f:
if mode == 'r':
return f.read()
elif mode == 'w' and data:
f.write(data)
return True
return False
class TestCore(unittest.TestCase):
dynamic_mock = DynamicMockOpen()
def setUp(self):
TestCore.dynamic_mock.add_mock_pattern(
filename_check=lambda filename: filename == "foo.txt",
read_data="foo content"
)
TestCore.dynamic_mock.add_mock_pattern(
filename_check=lambda filename: filename == "bar.txt"
)
self.assertTrue(TestCore.dynamic_mock.mock_patterns[0][0]("foo.txt"))
self.assertTrue(TestCore.dynamic_mock.mock_patterns[0][1]().read() == "foo content")
self.assertTrue(TestCore.dynamic_mock.mock_patterns[1][0]("bar.txt"))
# NOTE: we are using the new_callable again as the new method won't pass the self,
# but rather the args and kwargs
@mock.patch("builtins.open", new_callable=lambda: TestCore.dynamic_mock.mock_open)
def test_core_dynamic_mock(self, _):
# Test reading from "foo.txt"
result_foo = core("foo.txt")
self.assertEqual(result_foo, "foo content")
# Test writing to "bar.txt"
result_bar_write = core("bar.txt", mode='w', data="bar content")
self.assertTrue(DynamicMockOpen.get_written_data(self.dynamic_mock.mocks[1]) == 'bar content')
self.assertTrue(result_bar_write)
# Validate the mock calls
print("Mock calls for foo.txt:", self.dynamic_mock.get_mock_calls("foo.txt"))
print("Mock calls for bar.txt:", self.dynamic_mock.get_mock_calls("bar.txt"))
if __name__ == "__main__":
unittest.main()
Pros: Highly flexible and OO
Cons: High complexity