I have a program that needs to write different files with different content depending on some logic.
I tried to write a assert_file_written_with_content(filepath: str, content: str)
function that would access a mock_open and check that content had been attempted to be written to that filepath.
This is my attempt
@pytest.fixture(autouse=True)
def mock_open_file(self, mocker):
mocker.patch("builtins.open", mock_open(), create=True)
def assert_file_written_with_content(file_path, content):
"""When file is opened using builtins.open and this is mocked in the test, assert that the file is written with the given content."""
mock_open_file = builtins.open # This will be mock_open
for call in mock_open_file.call_args_list:
if call.args[0] == file_path and call.args[1] == "w":
call.return_value.write.assert_called_with(content)
return
raise AssertionError(f"File {file_path} was not written with content {content}")
For some reason, it is always passing; even when the content did not match.
Why is there no assertion error being raised when the content is wrong?
Bonus points if you can have the test print the comparison in filepaths and content if there's a mismatch when run through pytest.
I now have this, which seems to work, but also is quite hacky, I feel there must be a better way:
import builtins
from testfixtures import compare
@pytest.fixture(autouse=True)
def mock_open_file(self, mocker):
mocker.patch("builtins.open", mock_open(), create=True)
def assert_file_written_with_content(file_path, content):
"""When file is opened using builtins.open and this is mocked in the test, assert that the file is written with the given content."""
mock_open_file = builtins.open # This will be mock_open
for i, call in enumerate(mock_open_file.mock_calls[::4]):
current_iteration = i * 4
if call.args[0] == file_path and call.args[1] == "w":
call_write = mock_open_file.mock_calls[current_iteration + 2]
compare(content, call_write[1][0])
return
raise AssertionError(f"File {file_path} was not written with content {content}")
I'm a little confused by your code; in the second example you don't appear to be mocking out the open
method, and in the first example you have a mock_open_file
fixture but you don't appear to be using it.
The following example demonstrates one way to assert on calls to the write
method; there's both a passing test and a failing test to demonstrate that it works as expected:
from unittest import mock
def func_that_writes_a_file():
with open('myfile.txt', 'w') as fd:
fd.write('hello world\n')
def test_open_succeeds():
mock_open = mock.mock_open()
with mock.patch('builtins.open', mock_open):
func_that_writes_a_file()
assert mock_open.return_value.write.call_args[0][0] == 'hello world\n'
def test_open_fails():
mock_open = mock.mock_open()
with mock.patch('builtins.open', mock_open):
func_that_writes_a_file()
assert mock_open.return_value.write.call_args[0][0] == 'goodbye world\n'
It's also possible to assert on file content, rather than on calls to write
; this is useful if the content you're testing is created by multiple write
calls. In this version of the code, we create an io.StringIO()
object and use that as the return value of open
so that we can test content afterwards using the getvalue()
method:
import io
import pytest
from unittest import mock
def func_that_writes_a_file():
with open("myfile.txt", "w") as fd:
fd.write("hello world\n")
@pytest.fixture
def mock_open():
buffer = io.StringIO()
# We need to disable the `close` method, since we can't call
# getvalue() on a closed StringIO object.
buffer.close = lambda: None
mock_open = mock.mock_open()
mock_open.return_value = buffer
return mock_open
def test_open_succeeds(mock_open):
with mock.patch("builtins.open", mock_open):
func_that_writes_a_file()
assert mock_open.return_value.getvalue() == "hello world\n"
def test_open_fails(mock_open):
with mock.patch("builtins.open", mock_open):
func_that_writes_a_file()
assert mock_open.return_value.getvalue() == "goodbye world\n"
Update Based on your comment, it sounds like the easiest solution would be to use the pyfakefs
module, like this:
import io
import pytest
from unittest import mock
def func_that_writes_a_file():
with open("file1.txt", "w") as fd:
fd.write("hello world\n")
with open("file2.txt", "w") as fd:
fd.write("goodbye world\n")
def assert_content_written_to_filepath(content, path):
with open(path) as fd:
assert fd.read() == content
def test_open_succeeds(fs):
func_that_writes_a_file()
assert_content_written_to_filepath("hello world\n", "file1.txt")
assert_content_written_to_filepath("goodbye world\n", "file2.txt")
There's your assert_content_written_to_filepath
function. The fs
fixture provided by pyfakefs
takes care of mocking all the filesystem-related functions, so despite appearance we're not actually writing (or reading) from the regular filesystem.