Python unittest mock: можно ли имитировать значение аргументов метода по умолчанию во время тестирования?

У меня есть метод, который принимает аргументы по умолчанию:

def build_url(endpoint, host=settings.DEFAULT_HOST):
    return '{}{}'.format(host, endpoint)

У меня есть тестовый пример, который использует этот метод:

class BuildUrlTestCase(TestCase):
    def test_build_url(self):
        """ If host and endpoint are supplied result should be 'host/endpoint' """

        result = build_url('/end', 'host')
        expected = 'host/end'

        self.assertEqual(result,expected)

     @patch('myapp.settings')
     def test_build_url_with_default(self, mock_settings):
        """ If only endpoint is supplied should default to settings"""
        mock_settings.DEFAULT_HOST = 'domain'

        result = build_url('/end')
        expected = 'domain/end'

        self.assertEqual(result,expected)

Если я удалю точку отладки в build_url и проверю этот атрибут, settings.DEFAULT_HOST вернет фиктивное значение. Однако тест продолжает давать сбой, и утверждение указывает, что host присваивается значение из моего фактического settings.py. Я знаю, что это потому, что аргумент ключевого слова host устанавливается во время импорта, и мой макет не рассматривается.

отладчик

(Pdb) settings
<MagicMock name='settings' id='85761744'>                                                                                                                                                                                               
(Pdb) settings.DEFAULT_HOST
'domain'
(Pdb) host
'host-from-settings.com'                                                                                                                                                 

Есть ли способ переопределить это значение во время тестирования, чтобы я мог использовать путь по умолчанию с издевательским объектом settings?


person nsfyn55    schedule 03.06.2014    source источник
comment
Убедитесь, что вы исправляете правильный экземпляр myapp. Где определяется build_url? Вам может понадобиться что-то вроде @patch('module.myapp.settings'), а не @patch(myapp.settings).   -  person chepner    schedule 03.06.2014
comment
Да, беглый просмотр настроек в отладчике показывает, что он исправлен правильно (settings = <magicmock blah blah). Я предполагаю, что host=settings.DEFAULT_HOST произошло до патча.   -  person nsfyn55    schedule 03.06.2014
comment
О верно. Значение по умолчанию устанавливается при определении функции (по той же причине def foo(x=[]) не дает вам новый пустой список каждый раз, когда вы вызываете foo()). Что приводит к возможному ответу...   -  person chepner    schedule 03.06.2014


Ответы (3)


Функции сохраняют значения своих параметров по умолчанию в атрибуте func_defaults, когда функция определена, поэтому вы можете исправить это. Что-то вроде

def test_build_url(self):
    """ If only endpoint is supplied should default to settings"""

    # Use `func_defaults` in Python2.x and `__defaults__` in Python3.x.
    with patch.object(build_url, 'func_defaults', ('domain',)):
      result = build_url('/end')
      expected = 'domain/end'

    self.assertEqual(result,expected)

Я использую patch.object в качестве менеджера контекста, а не декоратора, чтобы избежать передачи ненужного объекта patch в качестве аргумента test_build_url.

person chepner    schedule 03.06.2014
comment
Заменит ли это build_url на Mock? Именно этот метод я хочу проверить. - person nsfyn55; 03.06.2014
comment
Упс, неважно. Работал как чемпион! Быстрое редактирование, вы не хотите исправлять строку 'build_url', а назначенную переменную. - person nsfyn55; 03.06.2014
comment
Сначала этот ответ выглядел великолепно, но сбил меня с толку по причине, указанной в ответе Джеффа О'Нила - person jdi; 28.03.2018
comment
Ой! Это так близко к тому, что я искал. Кто-нибудь знает, как вы можете сделать подобное с помощью метода __init__? Подобно @nsfyn55, я хочу изменить значение по умолчанию, но из конструктора :-/ Я попробовал трюк @chepner cleaver, но я думаю, что это невозможно сделать в статическом методе. - person Rastikan; 29.05.2018
comment
Когда я пытаюсь использовать путь вместо функции, @patch.object('some_module.my_function', '__defaults__', (22,)) я получаю AttributeError: some_model.my_function does not have the attribute '__defaults__' - person Boris; 17.03.2020
comment
patch.object принимает ссылку на фактический объект, а не полное имя как str, в качестве первого аргумента. @patch.object(some_module.my_function, '__defaults__', (22,)). - person chepner; 17.03.2020
comment
@chepner, что, если я хочу исправить аргументы по умолчанию функции, импортированной в тестируемую функцию? Я тестирую функцию my_module.main_function, которая также определяет my_module._minor_function, и я хочу изменить аргументы по умолчанию _minor_function, но использовать main_function в своем тесте. Тест конечно в отдельном файле. - person Boris; 17.03.2020
comment
Пожалуйста, опубликуйте новый вопрос, где вы можете более четко показать свой вариант использования. - person chepner; 17.03.2020
comment
@chepner stackoverflow.com/questions/60728285/ - person Boris; 17.03.2020

Я применил другой ответ на этот вопрос, но после менеджера контекста пропатченная функция была не такой, как раньше.

Моя исправленная функция выглядит так:

def f(foo=True):
    pass

В моем тесте я сделал это:

with patch.object(f, 'func_defaults', (False,)):

При вызове f после (не в) контекстного менеджера значение по умолчанию полностью исчезло, а не вернулось к предыдущему значению. Вызов f без аргументов дал ошибку TypeError: f() takes exactly 1 argument (0 given)

Вместо этого я просто сделал это перед своим тестом:

f.func_defaults = (False,)

И это после моего теста:

f.func_defaults = (True,)
person gaefan    schedule 11.02.2017

Альтернативный способ сделать это: используйте functools.partial, чтобы предоставить нужные вам аргументы «по умолчанию». технически это не то же самое, что их переопределение; вызывающая сторона видит явный аргумент, но вызывающая сторона не обязана его предоставлять. В большинстве случаев это достаточно близко, и после выхода из диспетчера контекста он делает все правильно:

# mymodule.py
def myfunction(arg=17):
    return arg

# test_mymodule.py
from functools import partial
from mock import patch

import mymodule

class TestMyModule(TestCase):
    def test_myfunc(self):
        patched = partial(mymodule.myfunction, arg=23)
        with patch('mymodule.myfunction', patched):
            self.assertEqual(23, mymodule.myfunction())  # Passes; default overridden
        self.assertEqual(17, mymodule.myfunction()) # Also passes; original default restored

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

person Andrew    schedule 27.02.2019
comment
Я не уверен, что это покрывает мой основной вариант использования. Я хочу проверить, что эта функция работает должным образом с параметром по умолчанию по сравнению с предоставленным kwarg. Если я просто симулирую предоставление kwarg, на самом деле ничего ценного не получится. Я мог бы просто создать тест, который предоставляет arg=23 и добиться того же общего эффекта. - person nsfyn55; 04.03.2019