nmcspadden
nmcspadden

Reputation: 55

How do I mock a a file IO so that I can override the name attribute in a unit test?

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

Answers (2)

chepner
chepner

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

blhsing
blhsing

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

Related Questions