How to retrieve all the content of calls made to a mock?

Question:

I’m writing a unit test for a function that takes an array of dictionaries and ends up saving it in a CSV. I’m trying to mock it with pytest as usual:

csv_output = (
    "NametSurnamern"
    "EvetFirstrn"
)
with patch("builtins.open", mock_open()) as m:
    export_csv_func(array_of_dicts)

assert m.assert_called_once_with('myfile.csv', 'wb') is None
[and here I want to gather all output sent to the mock "m" and assert it against "csv_output"]

I cannot get in any simple way all the data sent to the mock during the open() phase by csv to do the comparison in bulk, instead of line by line. To simplify things, I verified that the following code mimics the operations that export_csv_func() does to the mock:

with patch("builtins.open", mock_open()) as m:
  with open("myfile.csv", "wb") as f:
    f.write("NametSurnamern")
    f.write("EvetFirstrn")

When I dig into the mock, I see:

>>> m
<MagicMock name='open' spec='builtin_function_or_method' id='4380173840'>
>>> m.mock_calls
[call('myfile.csv', 'wb'),
 call().__enter__(),
 call().write('NametSurnamern'),
 call().write('EvetFirstrn'),
 call().__exit__(None, None, None)]
>>> m().write.mock_calls
[call('NametSurnamern'), call('EvetFirstrn')]
>>> dir(m().write.mock_calls[0])
['__add__'...(many methods), '_mock_from_kall', '_mock_name', '_mock_parent', 'call_list', 'count', 'index']

I don’t see anything in the MagickMock interface where I can gather all the input that the mock has received.

I also tried calling m().write.call_args but it only returns the last call (the last element of the mock_calls attribute, i.e. call('EvetFirstrn')).

Is there any way of doing what I want?

Asked By: Ender

||

Answers:

Indeed you can’t patch builtins.open.write directly since the patch within a with would need to enter the patched method and see that write is not a class method.

There are a bunch of solutions and the one I would think of first would be to use your own mock. See the example:

class MockOpenWrite:
    def __init__(self, *args, **kwargs):
        self.res = []

    # What's actually mocking the write. Name must match
    def write(self, s: str):
        self.res.append(s)

    # These 2 methods are needed specifically for the use of with.
    # If you mock using a decorator, you don't need them anymore.
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        return


mock = MockOpenWrite
with patch("builtins.open", mock):
    with open("myfile.csv", "w") as f:
        f.write("NametSurnamern")
        f.write("EvetFirstrn")

        print(f.res)

In that case, the res attribute is linked to the instance. So it disappears after the with closes.
You could eventually stored results somewhere else, like a global array, and check the results beyond the end of with.

Feel free to play around with your actual method.

Answered By: anteverse

You can create your own mock.call objects and compare them with what you have in the .call_args_list.

from unittest.mock import patch, mock_open, call

with patch("builtins.open", mock_open()) as m:
    with open("myfile.csv", "wb") as f:
        f.write("NametSurnamern")
        f.write("EvetFirstrn")

# Create your array of expected strings
expected_strings = ["NametSurnamern", "EvetFirstrn"]
write_calls = m().write.call_args_list
for expected_str in expected_strings:
    # assert that a mock.call(expected_str) exists in the write calls
    assert call(expected_str) in write_calls

Note that you can use the assert call of your choice. If you’re in a unittest.TestCase subclass, prefer to use self.assertIn.

Additionally, if you just want the arg values you can unpack a mock.call object as tuples. Index 0 is the *args. For example:

for write_call in write_calls:
    print('args: {}'.format(write_call[0]))
    print('kwargs: {}'.format(write_call[1]))
Answered By: wholevinski

I had to it this way (Python 3.9). It was quite tedious just to get the mock-args out of the function.

from somewhere import my_thing

@patch("lib.function", return_value=MagicMock())
    def test_my_thing(my_mock):
         my_thing(value1, value2)
         (value1_call_args, value2_call_args) = my_mock.call_args_list[0].args
Answered By: Ilmari Kumpula