Попытка смоделировать datetime.date.today (), но не работает

158

Может кто-нибудь сказать мне, почему это не работает?

>>> import mock
>>> @mock.patch('datetime.date.today')
... def today(cls):
...  return date(2010, 1, 1)
...
>>> from datetime import date
>>> date.today()
datetime.date(2010, 12, 19)

Возможно, кто-то может предложить лучший способ?

Бельмин Фернандес
источник
1
Документы mockбиблиотеки: voidspace.org.uk/python/mock/examples.html#partial-mocking
guettli
2
freezegun
Ullauri

Ответы:

125

Есть несколько проблем.

Во-первых, то, как вы используете mock.patch, не совсем правильно. При использовании в качестве декоратора он заменяет данную функцию / класс (в данном случае datetime.date.today) Mockобъектом только внутри декорированной функции . Таким образом, только внутри вас today()будет datetime.date.todayдругая функция, которая не соответствует вашим ожиданиям.

То, что вы действительно хотите, похоже, выглядит примерно так:

@mock.patch('datetime.date.today')
def test():
    datetime.date.today.return_value = date(2010, 1, 1)
    print datetime.date.today()

К сожалению, это не сработает:

>>> test()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "build/bdist.macosx-10.6-universal/egg/mock.py", line 557, in patched
  File "build/bdist.macosx-10.6-universal/egg/mock.py", line 620, in __enter__
TypeError: can't set attributes of built-in/extension type 'datetime.date'

Это терпит неудачу, потому что встроенные типы Python являются неизменяемыми - см. Этот ответ для более подробной информации.

В этом случае я бы создал подкласс datetime.date и создал бы правильную функцию:

import datetime
class NewDate(datetime.date):
    @classmethod
    def today(cls):
        return cls(2010, 1, 1)
datetime.date = NewDate

И теперь вы можете сделать:

>>> datetime.date.today()
NewDate(2010, 1, 1)
Даниэль Г
источник
13
хорошее решение, но, к сожалению, вызывает проблемы с травлением.
Бачек
14
Хотя этот ответ хорош, можно
смоделировать дату и время
Как бы вы восстановили datetimeэкземпляр к его первоначальному значению? с deepcoppy?
Олег Белоусов
5
Гораздо проще сделать:patch('mymodule.datetime', Mock(today=lambda: date(2017, 11, 29)))
Виктор Гавро
1
Гораздо проще сделать @patch('module_you_want_to_test.date', Mock( today=Mock(return_value=datetime.date(2017, 11, 29)))).
Джони Бибоп
163

Другой вариант - использовать https://github.com/spulec/freezegun/.

Установите это:

pip install freezegun

И использовать это:

from freezegun import freeze_time

@freeze_time("2012-01-01")
def test_something():

    from datetime import datetime
    print(datetime.now()) #  2012-01-01 00:00:00

    from datetime import date
    print(date.today()) #  2012-01-01

Это также влияет на другие вызовы datetime в вызовах методов из других модулей:

other_module.py:

from datetime import datetime

def other_method():
    print(datetime.now())    

main.py:

from freezegun import freeze_time

@freeze_time("2012-01-01")
def test_something():

    import other_module
    other_module.other_method()

И наконец:

$ python main.py
# 2012-01-01
Мехди Бехруз
источник
13
Очень очень полезная библиотека
Шон
3
Вы также можете попробовать python-libfaketime, если заметите, что ваши тесты freezegun работают медленно.
Саймон Вебер
Отличная библиотека, но, к сожалению, не очень хорошо работает с Google App Engine NDB / Datastore.
brandones
Мне нравится, что "freezegun" - это название библиотеки. Я действительно люблю разработчиков Python! :-D
MikeyE
Работает, но freezegun кажется медленным, особенно если у вас сложная логика с несколькими вызовами для текущего времени.
Андрей Беляк
115

Что бы это ни стоило, в документации Mock конкретно говорится о datetime.date.today, и это можно сделать без необходимости создания фиктивного класса:

https://docs.python.org/3/library/unittest.mock-examples.html#partial-mocking

>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
...     mock_date.today.return_value = date(2010, 10, 8)
...     mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
...     assert mymodule.date.today() == date(2010, 10, 8)
...     assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)
...
КПУП
источник
2
Это действительно не работает для меня. Хотя я ценю усилия по поиску записи.
Прадёт
8
что означает «mymodule» в функции патча?
seufagner
4
Нашел ссылку здесь под "Частичным издевательством"
Лео Си Хан
3
@seufagner mymodule объясняется довольно запутанно на voidspace.org.uk/python/mock/patch.html#where-to-patch . Кажется , что если ваш модуль использует from datetime import dateто имя модуля , в котором from datetime import dateи вызов date.today()появляется
данио
1
Спасибо. Работал! Пример: с mock.patch ('tests.views.datetime') как mock_date: mock_date.today.return_value = datetime.datetime (2016, 9, 18) mock_date.side_effect = lambda * args, ** kw: date (* args , ** квт)
Латрова
36

Думаю, я немного опоздал на это, но я думаю, что главная проблема здесь в том, что вы исправляете datetime.date.today напрямую, и, согласно документации, это неправильно.

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

Допустим, у вас есть файл functions.py, в котором есть следующее:

import datetime

def get_today():
    return datetime.date.today()

тогда, в вашем тесте, вы должны иметь что-то вроде этого

import datetime
import unittest

from functions import get_today
from mock import patch, Mock

class GetTodayTest(unittest.TestCase):

    @patch('functions.datetime')
    def test_get_today(self, datetime_mock):
        datetime_mock.date.today = Mock(return_value=datetime.strptime('Jun 1 2005', '%b %d %Y'))
        value = get_today()
        # then assert your thing...

Надеюсь, это поможет немного.

iferminm
источник
Это выглядит очень убедительно, но я не могу заставить это бежать (бросает NameError: name 'datetime' is not defined). Откуда берется datetime.strptimeссылка, Mock(return_value=...)если вы не импортируете datetimeв свой тестовый файл? ОБНОВЛЕНИЕ: Все в порядке, я просто пошел вперед и импортировал datetimeмодуль в тестовый файл. Я думал, что уловка была в том, что вы скрываете datetimeссылку из тестового файла.
imrek
@DrunkenMaster Мне бы хотелось увидеть пример того, что вы делали и над какой ссылкой вы издевались. ты делал import datetimeили from datetime import strptime? Если бы вы делали первый, вам нужно было бы смоделировать datetimeи сделать mocked_datetime.strptime.return_value = whatever, а потом - вам бы пришлось напрямую смоделировать ссылку strptime в файле, где живет тестируемый метод.
iferminm
@israelord Я хотел сказать, что в вашем последнем фрагменте кода (тестовом файле) отсутствует импорт ссылки на дату и время для Mock(return_value=datetime...)работы.
imrek
32

Чтобы добавить к решению Даниеля G :

from datetime import date

class FakeDate(date):
    "A manipulable date replacement"
    def __new__(cls, *args, **kwargs):
        return date.__new__(date, *args, **kwargs)

Это создает класс, который при создании экземпляра будет возвращать обычный объект datetime.date, но который также может быть изменен.

@mock.patch('datetime.date', FakeDate)
def test():
    from datetime import date
    FakeDate.today = classmethod(lambda cls: date(2010, 1, 1))
    return date.today()

test() # datetime.date(2010, 1, 1)
eternicode
источник
2
Будьте очень осторожны - вы должны использовать версию from, иначе вы можете получить странность, если вы используете datetime.date (или datetime или другие). IE - глубина стека достигнута, когда ваша фальшивая новая вызывает сама.
Дэнни Стейпл
У вас не будет этой проблемы, если фальшивый объект находится в своем собственном модуле: dpaste.com/790309 . Хотя, даже если он находится в том же модуле, что и смоделированная функция, он не импортирует date/ datetimeсам, он использует глобально доступную переменную, поэтому проблем не должно быть: dpaste.com/790310
eternicode
менее краткое объяснение можно найти здесь: williamjohnbert.com/2011/07/…
ezdazuzena
9

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

def get_date_now():
    return datetime.datetime.now()

Сегодня я узнал о FreezeGun , и, кажется, он прекрасно освещает этот случай

from freezegun import freeze_time
import datetime
import unittest


@freeze_time("2012-01-14")
def test():
    assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
Hito_kun
источник
9

Самый простой способ для меня это сделать:

import datetime
from unittest.mock import Mock, patch

def test():
    datetime_mock = Mock(wraps=datetime.datetime)
    datetime_mock.now.return_value = datetime.datetime(1999, 1, 1)
    with patch('datetime.datetime', new=datetime_mock):
        assert datetime.datetime.now() == datetime.datetime(1999, 1, 1)

ВНИМАНИЕ для этого решения: все функциональные возможности datetime moduleот target_moduleперестанут работать.

frx08
источник
1
Это действительно красиво и лаконично. Линия datetime_mock.now = Mock(return_value=datetime(1999, 1, 1)может быть даже сокращена до datetime_mock.now.return_value = datetime(1999, 1, 1). Вместо того, чтобы начинать патч с start(), рассмотрите возможность использования with patch(...):диспетчера контекста, чтобы убедиться, что datetimeон снова будет работать регулярно (без проверки), когда ваш тест закончится.
Дирк
Всегда отдавайте предпочтение решениям, использующим встроенную библиотеку
Nam G VU
@ frx08 Могу ли я узнать, как сбросить эту насмешку? Я имею в виду, как получить datetime.datetime.now()unmocked ^^?
Nam G VU
Хорошо после попытки использовать этот макет - одно ВНИМАНИЕ для этого решения - все функциональные datetime moduleвозможности target_moduleперестают работать.
Нам Г ВУ
1
Согласитесь, @ frx08 с with () уменьшит боль. Хотя внутри этого блока, например, дата, timedelta перестанет работать. Что, если нам теперь надо издеваться, но свидание с математикой все еще продолжается? Извините, у нас должен быть .now () только издевались над всем модулем datetime.
Nam G VU
7

Вы можете использовать следующий подход, основанный на решении Daniel G. У этого есть преимущество, не нарушая проверку типа с isinstance(d, datetime.date).

import mock

def fixed_today(today):
    from datetime import date

    class FakeDateType(type):
        def __instancecheck__(self, instance):
            return isinstance(instance, date)

    class FakeDate(date):
        __metaclass__ = FakeDateType

        def __new__(cls, *args, **kwargs):
            return date.__new__(date, *args, **kwargs)

        @staticmethod
        def today():
            return today

    return mock.patch("datetime.date", FakeDate)

По сути, мы заменим datetime.dateкласс на основе C нашим собственным подклассом python, который создает оригинальные datetime.dateэкземпляры и отвечает на isinstance()запросы точно так же, как и собственные datetime.date.

Используйте его как менеджер контекста в своих тестах:

with fixed_today(datetime.date(2013, 11, 22)):
    # run the code under test
    # note, that these type checks will not break when patch is active:
    assert isinstance(datetime.date.today(), datetime.date)

Подобный подход может быть использован для макетирования datetime.datetime.now()функции.

Андрей Лебедев
источник
Я не уверен, что это работает в Python 2.7. Я получаю максимальную глубину рекурсии RuntimeError с помощью __instancecheck__метода.
Дэн Лёвенхерц
Это действительно работает в Python 2.7, и это решило мою проблему с проверкой типа экземпляра, спасибо!
Каратеодоры
4

Вообще говоря, вы могли бы datetimeили, возможно, datetime.dateимпортировать в модуль где-нибудь. Более эффективный способ насмешки над этим методом - установить его на модуль, который его импортирует. Пример:

a.py

from datetime import date

def my_method():
    return date.today()

Затем для вашего теста сам фиктивный объект будет передан в качестве аргумента методу теста. Вы должны настроить макет с желаемым значением результата, а затем вызвать тестируемый метод. Тогда вы бы утверждали, что ваш метод сделал то, что вы хотите.

>>> import mock
>>> import a
>>> @mock.patch('a.date')
... def test_my_method(date_mock):
...     date_mock.today.return_value = mock.sentinel.today
...     result = a.my_method()
...     print result
...     date_mock.today.assert_called_once_with()
...     assert mock.sentinel.today == result
...
>>> test_my_method()
sentinel.today

Слово предупреждения. Безусловно, можно пойти за борт с насмешками. Когда вы это делаете, это делает ваши тесты более длинными, трудными для понимания и невозможными для обслуживания. Прежде чем издеваться над таким простым методом datetime.date.today, спросите себя, действительно ли вам нужно его издеваться. Если ваш тест является коротким и точным и работает нормально, без насмешки над функцией, вы можете просто смотреть на внутреннюю деталь кода, который вы тестируете, а не на объект, который вам нужно смоделировать.

jpmc26
источник
2

Вот еще один способ смоделировать datetime.date.today()с дополнительным бонусом, что остальные datetimeфункции продолжают работать, так как фиктивный объект настроен для обертывания исходного datetimeмодуля:

from unittest import mock, TestCase

import foo_module

class FooTest(TestCase):

    @mock.patch(f'{foo_module.__name__}.datetime', wraps=datetime)
    def test_something(self, mock_datetime):
        # mock only datetime.date.today()
        mock_datetime.date.today.return_value = datetime.date(2019, 3, 15)
        # other calls to datetime functions will be forwarded to original datetime

Обратите внимание на wraps=datetimeаргумент mock.patch()- когда foo_moduleиспользуются другие datetimeфункции, кроме того, что date.today()они будут перенаправлены в исходный упакованный datetimeмодуль.

MRTS
источник
1
Отличный ответ, в большинстве тестов, где вам нужно смоделировать дату, вам нужно использовать модуль datetime
Antoine Vo
1

Несколько решений обсуждаются в http://blog.xelnor.net/python-mocking-datetime/ . В итоге:

Макет объекта - простой и эффективный, но нарушает проверку isinstance ():

target = datetime.datetime(2009, 1, 1)
with mock.patch.object(datetime, 'datetime', mock.Mock(wraps=datetime.datetime)) as patched:
    patched.now.return_value = target
    print(datetime.datetime.now())

Ложный класс

import datetime
import mock

real_datetime_class = datetime.datetime

def mock_datetime_now(target, dt):
    class DatetimeSubclassMeta(type):
        @classmethod
        def __instancecheck__(mcs, obj):
            return isinstance(obj, real_datetime_class)

    class BaseMockedDatetime(real_datetime_class):
        @classmethod
        def now(cls, tz=None):
            return target.replace(tzinfo=tz)

        @classmethod
        def utcnow(cls):
            return target

    # Python2 & Python3 compatible metaclass
    MockedDatetime = DatetimeSubclassMeta('datetime', (BaseMockedDatetime,), {})

    return mock.patch.object(dt, 'datetime', MockedDatetime)

Использовать как:

with mock_datetime_now(target, datetime):
   ....
eddygeek
источник
0

Возможно, вы могли бы использовать свой собственный метод today (), который вы будете исправлять там, где это необходимо. Пример с насмешкой utcnow () можно найти здесь: https://bitbucket.org/k_bx/blog/src/tip/source/en_posts/2012-07-13-double-call-hack.rst?at=default

Константин Рыбников
источник
@JensTimmerman, пожалуйста, смотрите bitbucket.org/k_bx/blog/src/tip/source/en_posts/…
Константин Рыбников
0

Я реализовал метод @ user3016183, используя собственный декоратор:

def changeNow(func, newNow = datetime(2015, 11, 23, 12, 00, 00)):
    """decorator used to change datetime.datetime.now() in the tested function."""
    def retfunc(self):                             
        with mock.patch('mymodule.datetime') as mock_date:                         
            mock_date.now.return_value = newNow
            mock_date.side_effect = lambda *args, **kw: datetime(*args, **kw)
            func(self)
    return retfunc

Я думал, что это может помочь кому-то однажды ...

DainDwarf
источник
0

Можно макетировать функции из datetimeмодуля без добавленияside_effects

import mock
from datetime import datetime
from where_datetime_used import do

initial_date = datetime.strptime('2018-09-27', "%Y-%m-%d")
with mock.patch('where_datetime_used.datetime') as mocked_dt:
    mocked_dt.now.return_value = initial_date
    do()
Даниил Машкин
источник
0

Для тех из вас, кто использует pytest с mocker, вот как я высмеивал, datetime.datetime.now()что очень похоже на оригинальный вопрос.

test_get_now(mocker):
    datetime_mock = mocker.patch("blackline_accounts_import.datetime",)
    datetime_mock.datetime.now.return_value=datetime.datetime(2019,3,11,6,2,0,0)

    now == function_being_tested()  # run function

    assert now == datetime.datetime(2019,3,11,6,2,0,0)

По сути, макет должен быть установлен, чтобы вернуть указанную дату. Вы не можете напрямую связать объект datetime.

Даниэль Батлер
источник
0

Я сделал эту работу за счет импорта , datetimeкак realdatetimeи замена методов я нуждался в издеваться с реальными методами:

import datetime as realdatetime

@mock.patch('datetime')
def test_method(self, mock_datetime):
    mock_datetime.today = realdatetime.today
    mock_datetime.now.return_value = realdatetime.datetime(2019, 8, 23, 14, 34, 8, 0)
Адам МакКенна
источник
0

Вы можете издеваться, datetimeиспользуя это:

В модуле sources.py:

import datetime


class ShowTime:
    def current_date():
        return datetime.date.today().strftime('%Y-%m-%d')

В вашем tests.py:

from unittest import TestCase, mock
import datetime


class TestShowTime(TestCase):
    def setUp(self) -> None:
        self.st = sources.ShowTime()
        super().setUp()

    @mock.patch('sources.datetime.date')
    def test_current_date(self, date_mock):
        date_mock.today.return_value = datetime.datetime(year=2019, month=10, day=1)
        current_date = self.st.current_date()
        self.assertEqual(current_date, '2019-10-01')
MTMobile
источник
что sourcesу тебя в патче-декораторе?
елена
Уважаемая @elena, довольно сложно вспомнить, о чем я думала почти год назад)). Полагаю, я имел в виду просто любой модуль наших исходных кодов приложений - просто код вашего приложения.
MTMobile
0

CPython фактически реализует модуль datetime, используя как чистый Python Lib / datetime.py, так и C-оптимизированные модули / _datetimemodule.c . C-оптимизированная версия не может быть исправлена, но версия с чистым Python может.

В нижней части реализации чистого Python в Lib / datetime.py находится этот код:

try:
    from _datetime import *  # <-- Import from C-optimized module.
except ImportError:
    pass

Этот код импортирует все C-оптимизированные определения и эффективно заменяет все определения на чистом Python. Мы можем заставить CPython использовать чистую реализацию Python модуля datetime, выполнив:

import datetime
import importlib
import sys

sys.modules["_datetime"] = None
importlib.reload(datetime)

Установив sys.modules["_datetime"] = None, мы говорим Python игнорировать C-оптимизированный модуль. Затем мы перезагружаем модуль, который вызывает _datetimeсбой импорта из . Теперь определения чистого Python остаются и могут быть исправлены как обычно.

Если вы используете Pytest, включите приведенный выше фрагмент в conftest.py, и вы сможете исправлять datetimeобъекты обычным образом.

GrantJ
источник