Reputation: 1740
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 ?
Upvotes: 2
Views: 2468
Reputation: 16805
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())
Upvotes: 3