Пересмешка с помощью PyTest: улучшение моего XML модульного теста записи / анализа - PullRequest
0 голосов
/ 05 апреля 2020

Так что я довольно новичок с pytest и mock, но все еще имею опыт работы с junit и с насмешками mockito для 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()

Также вот pip-файл, используемый для этой python среды:

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

И это сработало бы. Как я могу это исправить?

1 Ответ

1 голос
/ 05 апреля 2020

Основная проблема с вашим подходом - это последовательность насмешек над объектами. Приспособление вызывается первым, и при издевательстве над парсером он использует не смоделированный etree, а реальный, в то время как в тесте парсер используется из смоделированного 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())

Наконец, если вы хотите использовать только приборы, вы можете сделать их зависимыми друг от друга:

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