Мне нужно исправить текущую дату и время в тестах. Я использую это решение:

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=***):
        ...

Как сделать это элегантно?

21
warvariuc 14 Дек 2015 в 09:40

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 по Поиск специального метода .

13
zvone 7 Окт 2018 в 19:54

[РЕДАКТИРОВАТЬ]

Возможно, самая интересная часть этого вопроса - Почему я не могу исправить 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).

13
Michele d'Amico 21 Дек 2015 в 12:30

Как прокомментировал вопрос, поскольку 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 был импортирован, и заменяет каждый экземпляр. Я предполагаю, что это спорно , можно ли сделать это элегантно в одной функции .

4
Dag Høidahl 18 Дек 2015 в 14:29