Тестирование Python: использование поддельного файла с mock и io.StringIO

Я пытаюсь протестировать некоторый код, который работает с файлом, и я не могу понять, как заменить использование реального файла на mock и io.StringIO. Мой код в значительной степени выглядит следующим образом:

class CheckConfig(object):
    def __init__(self, config):
        self.config = self._check_input_data(config)

    def _check_input_data(self, data):
        if isinstance(data, list):
            return self._parse(data)
        elif os.path.isfile(data):
            with open(data) as f:
                return self._parse(f.readlines())

    def _parse(self, data):
        return data

У меня есть класс, который может принимать либо список, либо файл, если это файл, он открывает его и извлекает содержимое в список, а затем делает то, что ему нужно сделать с результирующим списком.

У меня есть рабочий тест следующим образом:

def test_CheckConfig_with_file():
    config = 'config.txt'
    expected = parsed_file_data
    actual = CheckConfig(config).config
    assert expected == actual

Я хочу заменить вызов файловой системы. Я попытался заменить файл на io.StringIO, но я получаю TypeError из os.path.isfile(), так как он ожидает строку, байты или целое число. Я также пытался издеваться над методом isfile следующим образом:

@mock.patch('mymodule.os.path')
def test_CheckConfig_with_file(mock_path):
    mock_path.isfile.return_value = True
    config = io.StringIO('data')
    expected = parsed_file_data
    actual = CheckConfig(config).config
    assert expected == actual

но я все еще получаю то же самое TypeError, поскольку тип _io.StringIO вызывает исключение до того, как isfile получит возможность что-то вернуть.

Как я могу заставить os.path.isfile возвращать True, когда я передаю ему поддельный файл? Или это предложение изменить код?


person bordeltabernacle    schedule 03.11.2016    source источник
comment
Можете ли вы также вставить журнал ошибок?   -  person kiran.koduru    schedule 03.11.2016


Ответы (2)


Просто смоделируйте вызовы os.path.isfile и open() и передайте поддельное имя файла (в конце концов, вы не должны передавать открытый файл).

Библиотека макетов включает утилиту для последнего: mock_open(). :

@mock.patch('os.path.isfile')
def test_CheckConfig_with_file(mock_isfile):
    mock_isfile.return_value = True
    config_data = mock.mock_open(read_data='data')
    with mock.patch('mymodule.open', config_data) as mock_open:
        expected = parsed_file_data
        actual = CheckConfig('mocked/filename').config
        assert expected == actual

Это приводит к тому, что тест if isinstance(data, list): оказывается ложным (поскольку data вместо этого является строкой), за которым следует elif os.path.isfile(data):, возвращающий True, и вызов open(data) для использования ваших имитированных данных из результата mock_open().

Вы можете использовать переменную mock_open, чтобы утверждать, что open() был вызван с правильными данными (например, mock_open. assert_called_once_with('mocked/filename')).

Демо:

>>> import os.path
>>> from unittest import mock
>>> class CheckConfig(object):
...     def __init__(self, config):
...         self.config = self._check_input_data(config)
...     def _check_input_data(self, data):
...         if isinstance(data, list):
...             return self._parse(data)
...         elif os.path.isfile(data):
...             with open(data) as f:
...                 return self._parse(f.readlines())
...     def _parse(self, data):
...         return data
...
>>> with mock.patch('os.path.isfile') as mock_isfile:
...     mock_isfile.return_value = True
...     config_data = mock.mock_open(read_data='line1\nline2\n')
...     with mock.patch('__main__.open', config_data) as mock_open:
...         actual = CheckConfig('mocked/filename').config
...
>>> actual
['line1\n', 'line2\n']
>>> mock_open.mock_calls
[call('mocked/filename'),
 call().__enter__(),
 call().readlines(),
 call().__exit__(None, None, None)]
person Martijn Pieters    schedule 03.11.2016
comment
Отлично, спасибо! Это именно то, что мне было нужно, и это помогло мне лучше понять насмешки. Просто из интереса, есть ли причина, по которой вы используете диспетчер контекста, а не декоратор @mock.patch. В предыдущих тестах я использовал декоратор, но здесь я не мог найти способ, поскольку я использовал pytest.fixture для содержимого вызова open, который был недоступен до тех пор, пока не находился внутри тестовой функции. - person bordeltabernacle; 03.11.2016
comment
для тех, кто наткнулся на этот ответ. mock_open в настоящее время не поддерживает итерации, поэтому, если вместо f.readlines() вы использовали yield, вам придется обойти итерацию. stackoverflow.com/questions/24779893 / - person Luis Meraz; 02.05.2018
comment
Вторя комментарию bordeltabernacle. Основываясь на этом ответе, я мог издеваться над содержимым файла, используя декоратор или контекстный менеджер. Однако декоратор не поддерживал использование параметра в качестве идентификатора, поэтому я оказался в необычном положении, имея декоратор патча и только self параметр. - person John; 03.06.2020
comment
Единственная причина, по которой я использовал здесь диспетчер контекста, заключается в том, что с ним проще выполнять демонстрацию. Версия декоратора работает точно так же; декоратор просто настраивает диспетчер контекста и передает объект patch в качестве дополнительного аргумента украшенной функции. - person Martijn Pieters; 04.06.2020

Если вы в конечном итоге задаетесь вопросом, как решить эту проблему с помощью библиотеки pytest-mock, здесь как вы это делаете:

def test_open(mocker):
    m = mocker.patch('builtins.open', mocker.mock_open(read_data='bibble'))
    with open('foo') as h:
        result = h.read()

    m.assert_called_once_with('foo')
    assert result == 'bibble'

Этот пример кода был найден (но его пришлось скорректировать) здесь.

person Manu CJ    schedule 14.10.2020
comment
За исключением того, что это работает абсолютно для всего, включая шаблоны для ошибок и исключений, что может привести к удивительным результатам! :-) - person Rmatt; 04.11.2020