Як я знущаюся над відкритим, що використовується в операторі with (використовуючи Mock Framework в Python)?


188

Як перевірити наступний код з макетами (використовуючи макети, декоратор патчів та дозорні програми, надані Mock Framework Майкла Форада ):

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

@Daryl Spitzer: чи можете ви залишити мета-питання ("Я знаю відповідь ...") Це заплутано.
S.Lott

У минулому, коли я його відключив, люди скаржилися, що я відповідаю на власне запитання. Я спробую перенести це на свою відповідь.
Дарил Спітцер

1
@Daryl: Найкращий спосіб уникнути скарг на відповідь на власне запитання, які, як правило, виникають з приводу занепокоєння з приводу "побиття карми", - позначити це питання та / або відповісти як "вікі спільноти".
Джон Міллікін

3
Якщо відповідь на ваше власне питання вважається Кармою Уорінг, на цей погляд, я думаю, слід уточнити поширені запитання.
EBGreen

Відповіді:


132

Спосіб цього змінився в макеті 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 (як зазначено вище), тоді введення та вихід налаштовані для вас.
нечіткий

5
ви можете вказати на свій допис у блозі, де ви детальніше поясните, чому / як це працює
Rodrigue

9
У Python 3 "файл" не визначено (використовується у специфікації MagicMock), тому я використовую натомість io.IOBase.
Джонатан Хартлі

1
Примітка: у Python3 вбудованого fileнемає!
ексгума

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 () приймає довільні аргументи ключових слів. Вони будуть передані Макету (або 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", ...) як другий аргумент.
імрек

1
@DrunkenMaster Оновлено .. спасибі за вказане. Використання декоратора в цьому випадку не тривіально.
Мікеле д'Аміко

Grazie! Моя проблема була трохи складніше (я повинен був направити return_valueна mock_openв інший фіктивний об'єкт і які стверджують другий фіктивної - х return_value), але він працював, додаючи в mock_openякості new_callable.
імрек

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

1
Як знайти правильне ім’я для виправлення? Тобто, як я знаходжу перший аргумент на @patch (у цьому випадку "вбудований.закрити") для довільної функції?
zenperttu

73

В останніх версіях макету ви можете використовувати дійсно корисний помічник mock_open :

mock_open (макет = жоден, read_data = Жоден)

Допоміжна функція для створення макету для заміни використання відкритого. Він працює для відкритих викликів безпосередньо або використовується як менеджер контексту.

Аргумент макету є об'єктом макету, який потрібно налаштувати. Якщо немає (за замовчуванням), то для вас буде створений MagicMock, API обмежений методами чи атрибутами, доступними на стандартних ручках файлів.

read_data - рядок для повернення методу читання для файлу, що повертається. Це порожній рядок за замовчуванням.

>>> 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')краще imo. Уникає реєстрації дзвінка.
Анонім

2
Стверджувати вручну Mock.call_args_listбезпечніше, ніж викликати будь-який із Mock.assert_xxxметодів. Якщо ви неправильно написали будь-яке з останніх, будучи атрибутами Макету, вони завжди мовчки пройдуть.
Джонатан Хартлі

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
for line in f:Підтримка @EvgeniiPuchkaryov може бути досягнута, замість цього знущаючись із значення повернення 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). Виконання цього способу за допомогою двох макетів, описаних вище, дозволяє нам захопити аргументи, передані 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" з потрібними атрибутами.
користувач3289695

Чи є спосіб уникнути дотику __enter__? Це, безумовно, більше схоже на злом, ніж рекомендований спосіб.
imrek

enter - це те, як записуються менеджери conext, як open (). Знущання часто будуть трохи хиткими в тому, що вам потрібно отримати доступ до "приватних" речей, щоб знущатися, але введіть тут не задумливо хакі
іммо

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__якщо будете використовувати цитон.


Варіант цього підходу працював для мене, оскільки більшість тестованих кодів було в інших модулях, як показано тут. Мені потрібно було обов’язково додати 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.