I tried to mock the open function used in a method of my class. I found this thread How do I mock an open used in a with statement (using the Mock framework in Python)? but could not solve my issue. Also the unittest documention shows a solution which also didn't mock my open https://docs.python.org/3/library/unittest.mock-examples.html#patch-decorators
This is my class with the method where the open function is used:
#__init.py__
import json
class MyClass:
def save_data_to_file(self, data):
with open('/tmp/data.json', 'w') as file:
json.dump(data, file)
...
mc = MyClass()
Now I found a little different solution. This is my test:
#save_to_file_test.py
from mymodule import MyClass
from mock import mock_open, patch
import ast
class SaveToFileTest(unittest.TestCase):
def setUp(self):
self.mc = MyClass()
self.data = [
{'id': 5414470, 'name': 'peter'},
{'id': 5414472, 'name': 'tom'},
{'id': 5414232, 'name': 'pit'},
]
def test_save_data_to_file(self):
m = mock_open()
with patch('mymodule.open', m, create=True):
self.mc.save_data_to_file(self.data)
string = ''
for call in m.return_value.write.mock_calls:
string += (call[1][0])
list = ast.literal_eval(string)
assertEquals = (list, self.data)
I'm not sure if this is the best way to test the content which should be written to a file. When I test the mock_calls (call_args_list is the same) this are the arguments which are passed to the file handle. Any advice, improvements and suggestions are welcome.
Since you want to mock out the method with a custom implementation, you could just create a custom mock method object and swap out the original method at testing runtime. I want to do a custom implementation of loaddata method , rather than just do the return value. You can assign loaddata to the custom implementation.
Mock vs. So what is the difference between them? MagicMock is a subclass of Mock . It contains all magic methods pre-created and ready to use (e.g. __str__ , __len__ , etc.). Therefore, you should use MagicMock when you need magic methods, and Mock if you don't need them.
mock is a library for testing in Python. It allows you to replace parts of your system under test with mock objects and make assertions about how they have been used. unittest. mock provides a core Mock class removing the need to create a host of stubs throughout your test suite.
What is a Mock? Mock objects allow you to mimic the behavior of classes and interfaces, letting the code in the test interact with them as if they were real. This isolates the code you’re testing, ensuring that it works on its own and that no other code will make the tests fail.
In Python, to mock, be it functions, objects or classes, you will mostly use Mock class. Mock class comes from the built-in unittest.mock module. From now on, anytime you come across Mock, know that it is from the unittest library.
With the latest versions of mock, you can use the really useful mock_open helper: A helper function to create a mock to replace the use of open. It works for open called directly or used as a context manager. The mock argument is the mock object to configure.
While doing unit testing using junit you will come across places where you want to mock classes. Mocking is done when you invoke methods of a class that has external communication like database calls or rest calls. Through mocking you can explicitly define the return value of methods without actually executing the steps of the method.
The heart of your problem is that you should be also mocking json.dump
to be able to properly test the data that is going to be written to your file. I actually had a hard time running your code until a few important adjustments were made to your test method.
builtins.open
and not mymmodule.open
m.return_value.__enter__.write
, however you are actually calling the write from json.dump which is where the write will be called. (Details below on a suggested solution)json.dump
to simply validate it is called with your dataIn short, with the issues mentioned above, the method can be re-written as:
Details about all this below
def test_save_data_to_file(self):
with patch('builtins.open', new_callable=mock_open()) as m:
with patch('json.dump') as m_json:
self.mc.save_data_to_file(self.data)
# simple assertion that your open was called
m.assert_called_with('/tmp/data.json', 'w')
# assert that you called m_json with your data
m_json.assert_called_with(self.data, m.return_value)
To focus on the problems I see in your code, the first thing I strongly suggest doing, since open
is a builtin, is to mock from builtins, furthermore, you can save yourself a line of code by making use of new_callable
and as
, so you can simply do this:
with patch('builtins.open', new_callable=mock_open()) as m:
The next problem that I see with your code as I had trouble running this until I actually made the following adjustment when you started looping over your calls:
m.return_value.__enter__.return_value.write.mock_calls
To dissect that, what you have to keep in mind is that your method is using a context manager. In using a context manager, the work of your write will actually be done inside your __enter__
method. So, from the return_value
of your m
, you want to then get the return_value of __enter__
.
However, this brings us to the heart of the problem with what you are trying to test. Because of how the json.dump
works when writing to the file, your mock_calls
for your write after inspecting the code, will actually look like this:
<MagicMock name='open().write' id='4348414496'>
call('[')
call('{')
call('"name"')
call(': ')
call('"peter"')
call(', ')
call('"id"')
call(': ')
call('5414470')
call('}')
call(', ')
call('{')
call('"name"')
call(': ')
call('"tom"')
call(', ')
call('"id"')
call(': ')
call('5414472')
call('}')
call(', ')
call('{')
call('"name"')
call(': ')
call('"pit"')
call(', ')
call('"id"')
call(': ')
call('5414232')
call('}')
call(']')
call.__str__()
That is not going to be fun to test. So, this brings us to the next solution you can try out; Mock json.dump
.
You shouldn't be testing json.dump, you should be testing calling it with the right parameters. With that being said, you can follow similar fashion with your mocking and do something like this:
with patch('json.dump') as m_json:
Now, with that, you can significantly simplify your test code, to simply validate that the method gets called with your data that you are testing with. So, with that, when you put it all together, you will have something like this:
def test_save_data_to_file(self):
with patch('builtins.open', new_callable=mock_open()) as m:
with patch('json.dump') as m_json:
self.mc.save_data_to_file(self.data)
# simple assertion that your open was called
m.assert_called_with('/tmp/data.json', 'w')
# assert that you called m_json with your data
m_json.assert_called_with(self.data, m.return_value.__enter__.return_value)
If you're interested in further refactoring to make your test method a bit cleaner, you could also set up your patching as a decorator, leaving your code cleaner inside the method:
@patch('json.dump')
@patch('builtins.open', new_callable=mock_open())
def test_save_data_to_file(self, m, m_json):
self.mc.save_data_to_file(self.data)
# simple assertion that your open was called
m.assert_called_with('/tmp/data.json', 'w')
# assert that you called m_json with your data
m_json.assert_called_with(self.data, m.return_value.__enter__.return_value)
Inspecting is your best friend here, to see what methods are being called at what steps, to further help with the testing. Good luck.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With