2

So I am rather new with pytest and mock, but still have experience with junit and mocking with mockito for groovy (which comes with an handy when(...).thenAnswer/Return function)

I wrote a simple class to parse and write xml files. This class sole purpose for existence is to be mocked in order to unit test the plugin I am currently working on. This personal project is also used as a learning tool to help me in my work duties (devOps python based)

Obviously, I needed to test it too.

Here is the class:

from lxml import etree from organizer.tools.exception_tools import ExceptionPrinter class XmlFilesOperations(object): @staticmethod def write(document_to_write, target): document_to_write.write(target, pretty_print=True) @staticmethod def parse(file_to_parse): parser = etree.XMLParser(remove_blank_text=True) try: return etree.parse(file_to_parse, parser) except Exception as something_happened: ExceptionPrinter.print_exception(something_happened) 

And here is the unit test for it:

import mock from organizer.tools.xml_files_operations import XmlFilesOperations FILE_NAME = "toto.xml" @mock.patch('organizer.tools.xml_files_operations.etree.ElementTree') def test_write(mock_document): XmlFilesOperations.write(mock_document, FILE_NAME) mock_document.write.assert_called_with(FILE_NAME, pretty_print=True) @mock.patch('organizer.tools.xml_files_operations.etree') def test_parse(mock_xml): XmlFilesOperations.parse(FILE_NAME) mock_xml.parse.assert_called() 

Also, here is the pipfile used for this python environment:

[[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] lxml = "*" pytest = "*" pytest-lazy-fixture = "*" mock = "*" MKLpy = "*" 

I would like to improve this test by making use of the assert_called_with function in the test_parse function. However to make it work I need to get the exact parser that is used in the XmlFilesOperations.parse method so I imagined mocking it too. For this I need the etree.XMLParser(remove_blank_text=True) call to return a mocked object

Here is what I tried:

import mock import pytest from lxml import etree from organizer.tools.xml_files_operations import XmlFilesOperations FILE_NAME = "toto.xml" @pytest.fixture() def mock_parser(): parser = mock.patch('organizer.tools.xml_files_operations.etree.XMLParser').start() with mock.patch('organizer.tools.xml_files_operations.etree.XMLParser', return_value=parser): yield parser parser.stop() @mock.patch('organizer.tools.xml_files_operations.etree') def test_parse(mock_xml, mock_parser): XmlFilesOperations.parse(FILE_NAME) mock_xml.parse.assert_called_with(FILE_NAME, mock_parser) 

I obtain the following error:

 def raise_from(value, from_value): > raise value E AssertionError: expected call not found. E Expected: parse('toto.xml', <MagicMock name='XMLParser' id='65803280'>) E Actual: parse('toto.xml', <MagicMock name='etree.XMLParser()' id='66022384'>) 

So the mocked object returned by the call is not the same mocked object that I created.

With Mockito, I would have done something like this:

parser = etree.XmlParser() when(etree.XMLParser(any()).thenReturn(parser) 

And it would work. How could I fix that ?

1 Answer 1

3

The main problem with your approach is the sequence of mocking the objects. The fixture is called first, and while mocking the parser, it does not use the mocked etree, but the real one, while in the test the parser is used from the mocked etree, which is another mock created by that mock.
Additionally, you did check for the parser method instead of the parser itself.

Here is what should work without using a fixture:

@mock.patch('organizer.tools.xml_files_operations.etree.XMLParser') @mock.patch('organizer.tools.xml_files_operations.etree') def test_parse(mock_xml, mock_parser): XmlFilesOperations.parse(FILE_NAME) mock_xml.parse.assert_called_with(FILE_NAME, mock_parser()) 

Another possibility is to exchange the fixture and the patch, so that they are used in the correct order:

@pytest.fixture() def mock_etree(): with mock.patch('organizer.tools.xml_files_operations.etree') as mocked_etree: yield mocked_etree @mock.patch('organizer.tools.xml_files_operations.etree.XMLParser') def test_parse(mock_xml_parser, mock_etree): XmlFilesOperations.parse(FILE_NAME) mock_etree.parse.assert_called_with(FILE_NAME, mock_xml_parser()) 

Finally, if you want to use only fixtures, you can make them dependent on each other:

@pytest.fixture() def mock_etree(): with mock.patch('organizer.tools.xml_files_operations.etree') as mocked_etree: yield mocked_etree @pytest.fixture() def mock_parser(mock_etree): parser = mock.Mock() with mock.patch.object(mock_etree, 'XMLParser', parser): yield parser def test_parse(mock_parser, mock_etree): XmlFilesOperations.parse(FILE_NAME) mock_etree.parse.assert_called_with(FILE_NAME, mock_parser()) 
Sign up to request clarification or add additional context in comments.

3 Comments

Well it worked thank you, however I am still unsure I understand why !
The answer was admittedly not clear enough, as it did not show the main problem. I edited it to be hopefully clearer now.
Thanks ! I actually came to something very similar in another test, where mocking in a fixture was mandatory.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.