Мне нужно исправить текущую дату и время в тестах. Я использую это решение:
def _utcnow():
return datetime.datetime.utcnow()
def utcnow():
"""A proxy which can be patched in tests.
"""
# another level of indirection, because some modules import utcnow
return _utcnow()
Затем в моих тестах я делаю что-то вроде:
with mock.patch('***.utils._utcnow', return_value=***):
...
Но сегодня мне пришла идея, что я мог бы упростить реализацию, исправив __call__
функции utcnow
вместо дополнительного _utcnow
.
У меня это не работает:
from ***.utils import utcnow
with mock.patch.object(utcnow, '__call__', return_value=***):
...
Как сделать это элегантно?
3 ответа
Когда вы исправляете __call__
функции, вы устанавливаете атрибут __call__
этого экземпляра . Python фактически вызывает метод __call__
, определенный в классе.
Например:
>>> class A(object):
... def __call__(self):
... print 'a'
...
>>> a = A()
>>> a()
a
>>> def b(): print 'b'
...
>>> b()
b
>>> a.__call__ = b
>>> a()
a
>>> a.__call__ = b.__call__
>>> a()
a
Назначать что-либо a.__call__
бессмысленно.
Однако:
>>> A.__call__ = b.__call__
>>> a()
b
TLDR ;
a()
не вызывает a.__call__
. Он вызывает type(a).__call__(a)
.
Ссылки
Хорошее объяснение того, почему это происходит, можно найти в ответе на "почему type(x).__enter__(x)
вместо x.__enter__()
в Стандартная контекстная библиотека Python? ".
Это поведение описано в документации Python по Поиск специального метода .
[РЕДАКТИРОВАТЬ]
Возможно, самая интересная часть этого вопроса - Почему я не могу исправить somefunction.__call__
?
Поскольку функция не использует код __call__
, а __call__
(объект-метод-обертка) использует код функции.
Я не нахожу никакой документации об этом, но я могу доказать это (Python2.7):
>>> def f():
... return "f"
...
>>> def g():
... return "g"
...
>>> f
<function f at 0x7f1576381848>
>>> f.__call__
<method-wrapper '__call__' of function object at 0x7f1576381848>
>>> g
<function g at 0x7f15763817d0>
>>> g.__call__
<method-wrapper '__call__' of function object at 0x7f15763817d0>
Замените код f
на код g
:
>>> f.func_code = g.func_code
>>> f()
'g'
>>> f.__call__()
'g'
Конечно, ссылки f
и f.__call__
не изменены:
>>> f
<function f at 0x7f1576381848>
>>> f.__call__
<method-wrapper '__call__' of function object at 0x7f1576381848>
Восстановите исходную реализацию и скопируйте ссылки __call__
:
>>> def f():
... return "f"
...
>>> f()
'f'
>>> f.__call__ = g.__call__
>>> f()
'f'
>>> f.__call__()
'g'
Это не влияет на функцию f
. Примечание. В Python 3 вы должны использовать __code__
вместо func_code
.
Я надеюсь, что кто-то может указать мне на документацию, которая объясняет это поведение.
У вас есть способ обойти это: в utils
вы можете определить
class Utcnow(object):
def __call__(self):
return datetime.datetime.utcnow()
utcnow = Utcnow()
И теперь ваш патч может работать как шарм.
Следуйте первоначальному ответу, который я считаю даже лучшим способом реализовать ваши тесты.
У меня есть собственное золотое правило : никогда не исправляйте защищенные методы . В этом случае все немного гладче, потому что защищенный метод был введен только для тестирования, но я не могу понять, почему.
Настоящая проблема здесь в том, что вы не можете установить патч datetime.datetime.utcnow
напрямую (это расширение C, как вы написали в комментарии выше). Что вы можете сделать, это исправить datetime
, обернув стандартное поведение и переопределить функцию utcnow
:
>>> with mock.patch("datetime.datetime", mock.Mock(wraps=datetime.datetime, utcnow=mock.Mock(return_value=3))):
... print(datetime.datetime.utcnow())
...
3
Хорошо, это не совсем понятно и аккуратно, но вы можете представить свою собственную функцию, как
def mock_utcnow(return_value):
return mock.Mock(wraps=datetime.datetime,
utcnow=mock.Mock(return_value=return_value)):
И сейчас
mock.patch("datetime.datetime", mock_utcnow(***))
Делать то, что вам нужно, без какого-либо другого слоя и для каждого вида импорта.
Другим решением может быть импорт datetime
в utils
и исправление ***.utils.datetime
; это может дать вам некоторую свободу изменять ссылочную реализацию datetime
без изменения ваших тестов (в этом случае позаботьтесь также об изменении аргумента mock_utcnow()
wraps
).
Как прокомментировал вопрос, поскольку datetime.datetime написано на C, Mock не может заменить атрибуты класса (см. Насмешка от datetime.today Неда Батчелдера). Вместо этого вы можете использовать freezegun.
$ pip install freezegun
Вот пример:
import datetime
from freezegun import freeze_time
def my_now():
return datetime.datetime.utcnow()
@freeze_time('2000-01-01 12:00:01')
def test_freezegun():
assert my_now() == datetime.datetime(2000, 1, 1, 12, 00, 1)
Как вы упоминаете, альтернативой является отслеживание импорта каждого модуля datetime
и их исправление. По сути, это то, что делает freezegun . Он принимает объект, имитирующий datetime
, перебирает sys.modules
, чтобы найти, куда datetime
был импортирован, и заменяет каждый экземпляр. Я предполагаю, что это спорно , можно ли сделать это элегантно в одной функции .
Похожие вопросы
Связанные вопросы
Новые вопросы
python
Python - это многопарадигмальный, динамически типизированный, многоцелевой язык программирования. Он разработан для быстрого изучения, понимания и использования, а также для обеспечения чистого и единообразного синтаксиса. Обратите внимание, что Python 2 официально не поддерживается с 01.01.2020. Тем не менее, для вопросов о Python, связанных с версией, добавьте тег [python-2.7] или [python-3.x]. При использовании варианта Python (например, Jython, PyPy) или библиотеки (например, Pandas и NumPy) включите его в теги.