Reputation: 55
I'm trying to write a unit test mock opening a file and passing it into a function that uses it to dump a JSON object into. How do I create a fake IO object that mimics the behavior of an open file handle but uses similar attributes, specifically .name
?
I've read through tons of answers on here and all of them work around the problem in various ways. I've tried mock patching builtins.open
, I've tried mock patching the open
being called inside my module, but the main error I keep running into is that when I try to access the fake IO object's .name
attribute, I get an AttributeError:
AttributeError: 'CallbackStream' object has no attribute 'name'
So here's a simple function that writes a dictionary to disk in JSON format and takes an open file handle:
def generate(data, json_file):
# data is a dict
logging.info(f"Writing out spec file to {json_file.name}")
json.dump(data, json_file)
Here's what I've tried to unit test:
@patch("builtins.open", new_callable=mock_open())
def test_generate_json_returns_zero(self, mock_open):
mocked_file = mock_open()
mocked_file.name = "FakeFileName"
data = {'stuff': 'stuff2'}
generate(data, json_file=mocked_file)
However, that produces the AttributeError above, where I can't use json_file.name
because it doesn't exist as an attribute. I thought setting it explicitly would work, but it didn't.
I can bypass the issue by using a temporary file, via `tempfile.TemporaryFile:
def test_generate_json_returns_zero(self, mock_open):
data = {'stuff': 'stuff2'}
t = TemporaryFile("w")
generate(data, json_file=t)
But that doesn't solve the actual problem, which is that I can't figure out how to mock the file handle so that I don't actually need to create a real file on disk.
I can't get past the .name
not being a valid attribute. How do I mock the file object such that I could actually use the .name
attribute of an IO object and still fake a json.dump()
to it?
Upvotes: 4
Views: 1644
Reputation: 531095
Your test never actually calls open
, so there's no need to patch it. Just create a Mock
instance with the attribute you need.
def test_generate_json_returns_zero(self):
mocked_file = Mock()
mocked_file.name = "FakeFileName"
data = {'stuff': 'stuff2'}
generate(data, json_file=mocked_file)
Upvotes: 1
Reputation: 106543
The new_callable
parameter is meant to be an alternative class of Mock
, so when you call:
@patch("builtins.open", new_callable=mock_open())
It patches builtins.open
by replacing it with what mock_open()
returns, rather than a MagicMock
object, which is what you actually need, so change the line to simply:
@patch("builtins.open")
and it should work.
Upvotes: 2