Настройка unittest.mock.mock_open для итерации

Как мне настроить unittest.mock.mock_open для обработки этого кода?

file: impexpdemo.py
def import_register(register_fn):
    with open(register_fn) as f:
        return [line for line in f]

Моя первая попытка попыталась read_data.

class TestByteOrderMark1(unittest.TestCase):
    REGISTER_FN = 'test_dummy_path'
    TEST_TEXT = ['test text 1\n', 'test text 2\n']

    def test_byte_order_mark_absent(self):
        m = unittest.mock.mock_open(read_data=self.TEST_TEXT)
        with unittest.mock.patch('builtins.open', m):
            result = impexpdemo.import_register(self.REGISTER_FN)
            self.assertEqual(result, self.TEST_TEXT)

Это не удалось, предположительно потому, что код не использует чтение, чтение или чтение. документация для unittest.mock.mock_open говорит:" read_data - это строка для методов read(), readline() и readlines() для дескриптора файла Вызов этих методов будет принимать данные из read_data до тех пор, пока он не будет исчерпан. Макет этих методов довольно упрощен. Если вам нужно больше контролировать данные, которые вы подаете на тестируемый код, вам нужно будет настроить этот макет для себя. По умолчанию read_data - это пустая строка.

Поскольку в документации не было намека на то, какая настройка потребуется, я попробовал return_value и side_effect. Ничего не работало.

class TestByteOrderMark2(unittest.TestCase):
    REGISTER_FN = 'test_dummy_path'
    TEST_TEXT = ['test text 1\n', 'test text 2\n']

    def test_byte_order_mark_absent(self):
        m = unittest.mock.mock_open()
        m().side_effect = self.TEST_TEXT
        with unittest.mock.patch('builtins.open', m):
            result = impexpdemo.import_register(self.REGISTER_FN)
            self.assertEqual(result, self.TEST_TEXT)

Ответ 1

Объект mock_open() действительно не реализует итерацию.

Если вы не используете объект файла в качестве диспетчера контекста, вы можете использовать:

m = unittest.mock.MagicMock(name='open', spec=open)
m.return_value = iter(self.TEST_TEXT)

with unittest.mock.patch('builtins.open', m):

Теперь open() возвращает итератор, что-то, что можно итерировать напрямую, как это может быть файловый объект, и он также будет работать с next(). Однако его нельзя использовать в качестве менеджера контекста.

Вы можете объединить это с mock_open(), а затем предоставить метод __iter__ и __next__ для возвращаемого значения, с дополнительным преимуществом, что mock_open() также добавляет предпосылки для использования в качестве менеджера контекста:

# Note: read_data must be a string!
m = unittest.mock.mock_open(read_data=''.join(self.TEST_TEXT))
m.return_value.__iter__ = lambda self: self
m.return_value.__next__ = lambda self: next(iter(self.readline, ''))

Возвращаемое значение здесь - это объект MagicMock, определенный из объекта file (Python 2) или файловых объектов в памяти (Python 3), но только read, [ Методы TG412] и __enter__ были заглушены.

Вышеупомянутое не работает в Python 2, потому что a) Python 2 ожидает, что next существует, а не __next__ и b) next не рассматривается как специальный метод в Mock (правильно), поэтому даже если вы переименовали __next__ - next в приведенном выше примере тип возвращаемого значения не будет иметь метода next. В большинстве случаев было бы достаточно, чтобы файловый объект создавал итерацию, а не итератор с:

# Python 2!
m = mock.mock_open(read_data=''.join(self.TEST_TEXT))
m.return_value.__iter__ = lambda self: iter(self.readline, '')

Будет работать любой код, который использует iter(fileobj) (включая цикл for).

Существует открытый вопрос в трекере Python, цель которого - устранить этот пробел.

Ответ 2

Как и в случае с Python 3.6, смещенный файловый объект, возвращаемый методом unittest.mock_open не поддерживает итерацию. Эта ошибка была сообщена в 2014 году, и она остается открытой с 2017 года.

Таким образом, код, подобный этому, беззвучно дает нулевые итерации:

f_open = unittest.mock.mock_open(read_data='foo\nbar\n')
f = f_open('blah')
for line in f:
  print(line)

Вы можете обойти это ограничение, добавив метод к издеваемому объекту, который возвращает правильный итератор строки:

def mock_open(*args, **kargs):
  f_open = unittest.mock.mock_open(*args, **kargs)
  f_open.return_value.__iter__ = lambda self : iter(self.readline, '')
  return f_open