Как смоделировать open, используемый в операторе with (используя платформу Mock в Python)?


188

Как протестировать следующий код с помощью mock (используя mocks, декоратор патчей и часовые, предоставляемые фреймворком Michael Foord ):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()

@Daryl Spitzer: не могли бы вы оставить мета-вопрос («Я знаю ответ ...») Это сбивает с толку.
S.Lott

В прошлом, когда я отказывался от этого, люди жаловались, что я отвечаю на свой вопрос. Я постараюсь перенести это в мой ответ.
Дэрил Спитцер

1
@Daryl: Лучший способ избежать жалоб на ответ на свой вопрос, который обычно возникает из-за беспокойства о «кармическом поклонении», состоит в том, чтобы пометить вопрос и / или ответ как «вики сообщества».
Джон Милликин

3
Если ответ на ваш собственный вопрос считается кармическим ворингом, я думаю, что часто задаваемые вопросы следует прояснить по этому вопросу.
EBGreen

Ответы:


132

Способ сделать это изменился в mock 0.7.0, который, наконец, поддерживает насмешку над методами протокола python (магические методы), особенно с использованием MagicMock:

http://www.voidspace.org.uk/python/mock/magicmock.html

Пример макета, открытого как менеджер контекста (со страницы примеров в документации макета):

>>> open_name = '%s.open' % __name__
>>> with patch(open_name, create=True) as mock_open:
...     mock_open.return_value = MagicMock(spec=file)
...
...     with open('/some/path', 'w') as f:
...         f.write('something')
...
<mock.Mock object at 0x...>
>>> file_handle = mock_open.return_value.__enter__.return_value
>>> file_handle.write.assert_called_with('something')

Вот Это Да! Это выглядит намного проще, чем пример менеджера контекста, который в настоящее время находится по адресу voidspace.org.uk/python/mock/magicmock.html, который также явно устанавливает __enter__и имитирует__exit__ объекты - является ли последний подход устаревшим или все еще полезным?
Брэндон Родс

6
«Последний подход» показывает, как это сделать без использования MagicMock (т.е. это всего лишь пример того, как Mock поддерживает магические методы). Если вы используете MagicMock (как указано выше), то вход и выход предварительно настроены для вас.
fuzzyman

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

9
В Python 3 «файл» не определен (используется в спецификации MagicMock), поэтому вместо него я использую io.IOBase.
Джонатан Хартли

1
Примечание: в Python3 встроенная функция fileисчезла!
exhuma

239

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

Python 3.x

Используйте builtinsвместо __builtin__.

from unittest.mock import patch, mock_open
with patch("builtins.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Python 2.7

mockне является частью, unittestи вы должны исправить__builtin__

from mock import patch, mock_open
with patch("__builtin__.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Чехол для декоратора

Если вы будете использовать в patchкачестве декоратора, используя mock_open()результат в качестве new patchаргумента, это может быть немного странно.

В этом случае лучше использовать new_callable patchаргумент 's' и помнить, что все лишние аргументы, которые patchне используются, будут переданы в new_callableфункцию, как описано в patchдокументации .

patch () принимает произвольные аргументы ключевого слова. Они будут переданы в Mock (или new_callable) при создании.

Например, оформленная версия для Python 3.x :

@patch("builtins.open", new_callable=mock_open, read_data="data")
def test_patch(mock_file):
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Помните, что в этом случае patchвы добавите фиктивный объект в качестве аргумента вашей тестовой функции.


Извините за вопрос, можно with patch("builtins.open", mock_open(read_data="data")) as mock_file:ли преобразовать в синтаксис декоратора? Я пытался, но я не уверен, что мне нужно передать в @patch("builtins.open", ...) качестве второго аргумента.
imrek

1
@DrunkenMaster Обновлено .. спасибо, что указал. Использование декоратора в этом случае не тривиально.
Мишель д'Амико

Grazie! Моя проблема была немного сложнее (я должен был направить return_valueна mock_openв другой фиктивный объект и утверждающие второй фиктивной - х return_value), но он работал, добавляя в mock_openкачестве new_callable.
imrek

1
@ArthurZopellaro взгляните на sixмодуль, чтобы иметь согласованный mockмодуль. Но я не знаю, отображается ли он также builtinsв общем модуле.
Мишель д'Амико

1
Как найти правильное имя для патча? Т.е. как найти первый аргумент @patch (в данном случае «builtins.open») для произвольной функции?
zenperttu

73

В последних версиях mock вы можете использовать действительно полезный помощник mock_open :

mock_open (mock = Нет, read_data = Нет)

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

Аргумент mock - это фиктивный объект для настройки. Если None (по умолчанию), то для вас будет создан MagicMock с API, ограниченным методами или атрибутами, доступными в стандартных файловых дескрипторах.

read_data - это строка, которую должен вернуть метод read дескриптора файла. Это пустая строка по умолчанию.

>>> from mock import mock_open, patch
>>> m = mock_open()
>>> with patch('{}.open'.format(__name__), m, create=True):
...    with open('foo', 'w') as h:
...        h.write('some stuff')

>>> m.assert_called_once_with('foo', 'w')
>>> handle = m()
>>> handle.write.assert_called_once_with('some stuff')

как проверить, есть ли несколько .writeзвонков?
n611x007

1
@naxa Один из способов - передать каждый ожидаемый параметр handle.write.assert_any_call(). Вы также можете использовать handle.write.call_args_listдля получения каждого звонка, если порядок важен.
Роб Катмор

m.return_value.write.assert_called_once_with('some stuff')лучше имо. Избегает регистрации звонка.
Аноним

2
Утверждать о ручном Mock.call_args_listбезопаснее, чем вызывать любой из Mock.assert_xxxметодов. Если вы неправильно произнесете заклинание, будучи атрибутом Mock, они всегда будут молчаливо проходить.
Джонатан Хартли

12

Чтобы использовать mock_open для простого файла read()(исходный фрагмент mock_open, уже приведенный на этой странице , предназначен для записи):

my_text = "some text to return when read() is called on the file object"
mocked_open_function = mock.mock_open(read_data=my_text)

with mock.patch("__builtin__.open", mocked_open_function):
    with open("any_string") as f:
        print f.read()

Обратите внимание, что в соответствии с документами для mock_open, это специально для read(), поэтому не будет работать с общими шаблонами, такими как for line in f, например.

Использует python 2.6.6 / mock 1.0.1


Выглядит хорошо, но я не могу заставить его работать с for line in opened_file:типом кода. Я попытался поэкспериментировать с итеративным StringIO, который реализует __iter__и использует его вместо my_text, но не повезло.
Евгений

@EvgeniiPuchkaryov Это работает специально для, read()так что не будет работать в вашем for line in opened_fileслучае; Я отредактировал пост, чтобы уточнить
jlb83

1
@EvgeniiPuchkaryov for line in f:поддержка может быть достигнута путем насмешливого возвращаемое значение , open()как объект а вместо StringIO .
Искар Джарак

1
Чтобы уточнить, тестируемая система (SUT) в этом примере: with open("any_string") as f: print f.read()
Брэд М

4

Верхний ответ полезен, но я немного расширил его.

Если вы хотите установить значение вашего файлового объекта ( fin as f) на основе аргументов, переданных open()вот один из способов сделать это:

def save_arg_return_data(*args, **kwargs):
    mm = MagicMock(spec=file)
    mm.__enter__.return_value = do_something_with_data(*args, **kwargs)
    return mm
m = MagicMock()
m.side_effect = save_arg_return_array_of_data

# if your open() call is in the file mymodule.animals 
# use mymodule.animals as name_of_called_file
open_name = '%s.open' % name_of_called_file

with patch(open_name, m, create=True):
    #do testing here

По сути, open()вернет объект и withвызовет __enter__()этот объект.

Чтобы правильно издеваться, мы должны open()возвращать ложный объект. Этот фиктивный объект должен затем имитировать __enter__()вызов ( MagicMockбудет делать это для нас), чтобы вернуть фиктивный объект данных / файлов, который мы хотим (следовательно mm.__enter__.return_value). Выполнение этого с помощью 2 mocks описанным выше способом позволяет нам захватывать передаваемые аргументы open()и передавать их нашему do_something_with_dataметоду.

Я передал весь макет файла в виде строки, open()и мой do_something_with_dataбыл похож на это:

def do_something_with_data(*args, **kwargs):
    return args[0].split("\n")

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

for line in file:
    #do action

Если тестируемый код обрабатывает файл другим способом, например, вызывая его функцию «readline», вы можете вернуть любой фиктивный объект в функцию «do_something_with_data» с требуемыми атрибутами.
user3289695

Есть ли способ избежать прикосновения __enter__? Это определенно больше похоже на взлом, чем рекомендуемый способ.
imrek

enter - это то, как пишутся conext менеджеры, такие как open (). Насмешки часто бывают немного хакерскими, потому что вам нужно получить доступ к «личным» вещам, чтобы издеваться, но вход здесь не слишком хакерский,
theannouncer

3

Возможно, я немного опоздал к игре, но это сработало для меня при вызове openдругого модуля без необходимости создания нового файла.

test.py

import unittest
from mock import Mock, patch, mock_open
from MyObj import MyObj

class TestObj(unittest.TestCase):
    open_ = mock_open()
    with patch.object(__builtin__, "open", open_):
        ref = MyObj()
        ref.save("myfile.txt")
    assert open_.call_args_list == [call("myfile.txt", "wb")]

MyObj.py

class MyObj(object):
    def save(self, filename):
        with open(filename, "wb") as f:
            f.write("sample text")

Прикрепив openфункцию внутри __builtin__модуля к моей mock_open(), я могу издеваться над записью в файл, не создавая его.

Примечание. Если вы используете модуль, который использует Cython, или ваша программа каким-либо образом зависит от Cython, вам нужно будет импортировать модуль Cython,__builtin__ включив его import __builtin__в начало вашего файла. Вы не сможете издеваться над универсальным, __builtin__если вы используете Cython.


Вариант этого подхода работал для меня, так как большая часть тестируемого кода была в других модулях, как показано здесь. Мне нужно было обязательно добавить import __builtin__в мой тестовый модуль. Эта статья помогла выяснить, почему эта техника работает так же хорошо, как и она: ichimonji10.name/blog/6
killthrush

0

Чтобы исправить встроенную функцию open () с помощью unittest:

Это сработало для патча для чтения конфига json.

class ObjectUnderTest:
    def __init__(self, filename: str):
        with open(filename, 'r') as f:
            dict_content = json.load(f)

Перемещаемый объект - это объект io.TextIOWrapper, возвращаемый функцией open ().

@patch("<src.where.object.is.used>.open",
        return_value=io.TextIOWrapper(io.BufferedReader(io.BytesIO(b'{"test_key": "test_value"}'))))
    def test_object_function_under_test(self, mocker):

0

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

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.