首页 > 解决方案 > 使用 PyTest 进行模拟:改进我的 XML 写入/解析单元测试

问题描述

所以我对 and 比较pytest陌生mock,但仍然有经验junit和嘲笑mockitofor groovy(它带有一个方便的when(...).thenAnswer/Return功能)

我写了一个简单的类来解析和编写 xml 文件。这个类存在的唯一目的是为了对我目前正在开发的插件进行单元测试。这个个人项目也被用作一个学习工具来帮助我完成我的工作职责(基于 devOps python)

显然,我也需要测试它。

这是课程:

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)

这是它的单元测试:

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()

此外,这里是用于此 python 环境的 pipfile:

[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[packages]
lxml = "*"
pytest = "*"
pytest-lazy-fixture = "*"
mock = "*"
MKLpy = "*"

我想通过使用assert_called_with函数中的test_parse函数来改进这个测试。然而,为了让它工作,我需要得到该XmlFilesOperations.parse方法中使用的确切解析器,所以我也想模拟它。为此,我需要etree.XMLParser(remove_blank_text=True)调用返回一个模拟对象

这是我尝试过的:

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)

我收到以下错误:

    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'>)

所以调用返回的模拟对象与我创建的模拟对象不同。

使用 Mockito,我会做这样的事情:

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

它会起作用。我该如何解决?

标签: xmlunit-testingmockingpytest

解决方案


您的方法的主要问题是模拟对象的顺序。夹具首先被调用,在模拟解析器时,它不使用 mocked etree,而是真正的,而在测试中,解析器是从 mocked 使用的etree,这是由该模拟创建的另一个模拟。
此外,您确实检查了解析器方法而不是解析器本身。

以下是不使用固定装置的情况:

@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())

另一种可能性是交换夹具和补丁,以便以正确的顺序使用它们:

@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())

最后,如果你只想使用fixtures,你可以让它们相互依赖:

@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())

推荐阅读