问题
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:
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())
来源:https://stackoverflow.com/questions/61037140/mocking-with-pytest-improving-my-xml-write-parse-unit-test