1

I'm trying to test a function in which one call results in multiple files being written:

def pull_files(output_files=[]):
    for output_file in output_files:
        content = get_content_from_server(output_file)
        with open('/output/' + output_file, "wb") as code:
            code.write(content)

I want my test to check that each call was made to open as expected, and that the content was written:

def test_case(self):
    pull_files("file1.txt", "file2.txt")

    # Assert open("file1.txt", "wb") was called
    # Assert "file 1 content" was written to "file1.txt"

    # Assert open("file2.txt", "wb") was called
    # Assert "file 2 content" was written to "file2.txt"

I've seen an example of handling two files here: Python mock builtin 'open' in a class using two different files

But I can't wrap my head around how to track what is actually written to them.

Andy
  • 3,228
  • 8
  • 40
  • 65
  • You would have to clear the folder you were writing to, write to the files, then open the files and read them, then compare what was read to what was expected. Another option (if this is being used in a configuration sense) is a round trip test where you load from a configuration, then write the configuration back to another file to test that they are the same (essentially that you are saving properly.) – Ryan Schaefer Mar 02 '20 at 15:41

2 Answers2

1

Here's an example mocking open and returning a StringIO as context:

from io import StringIO

def my_function(*fns):
    for i, fn in enumerate(fns):
        with open(fn, "wt") as fp:
            fp.write("content %d" % i)


string_io_one = StringIO()
string_io_two = StringIO()
with mock.patch("%s.open" % __name__) as open_mock:
    open_mock.return_value.__enter__.side_effect = [string_io_one, string_io_two]
    my_function("file1.txt", "file2.txt")

    assert open_mock.called_with("file1.txt")
    string_io_one.seek(0)
    assert string_io_one.read() == "content 0"
    assert open_mock.called_with("file2.txt")
    string_io_two.seek(0)
    assert string_io_two.read() == "content 1"

Similarly you could mock out "regular" use of open (without a context manager).

Edits made: Changed to cover the test cases of the original question.

sim
  • 1,227
  • 14
  • 20
0

First, you should never use a mutable object as your default argument to a function, which is an anti-pattern. You should change your function signature to def pull_files(output_files=()) instead.

Then, to your question, you can do a os.chdir to /tmp/ and make a temporary directory, then write files in the temporary folder instead. Don't forget to change your working directory back to what it was after the test.

Another solution is to modify your function slightly so that you are not prepending a prefix ('/output/' + output_file). This way, you can pass an io.BytesIO object instead of a path, which will let you modify the contents in-memory.

Mia
  • 2,466
  • 22
  • 38
  • `def pull_files(output_files = None):` surely? – Dan Mar 02 '20 at 15:49
  • @Dan yeah I just edited the answer and changed it to a tuple instead. `None` also works but if OP really doesn't want to do an extra condition check then a tuple might be better, since it's also an iterable. – Mia Mar 02 '20 at 15:53
  • Defaulting to None with the extra if condition to set the default is a very standard python pattern. I don't see any value in deviating from it. It is the pattern recommended in the link you've posted. – Dan Mar 02 '20 at 16:13
  • @Dan I agree it's a standard practice. The only reason that I changed it to a tuple is that it will not break OP's code if they simply copy-paste my suggestion. – Mia Mar 02 '20 at 16:16