Спробуємо знущатися над timetime.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

Відповіді:


124

Є кілька проблем.

Перш за все, спосіб, який ви використовуєте 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
приємне рішення, але, на жаль, викликає проблеми з маринуванням.
Baczek

14
Хоча ця відповідь хороший, можна знущатися дати і часу без створення класу: stackoverflow.com/a/25652721/117268
Еміль Штенштрём

Як би ви відновили 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

Це також впливає на інші дзвінки з датою у викликах методів з інших модулів:

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, якщо помітите, що ваші тести на заморожуванні працюють повільно.
Саймон Вебер

Чудова бібліотека, але, на жаль, не дуже добре працює з NDB / Datastore Google App Engine.
брендони

Мені подобається, що "freezegun" - це назва бібліотеки. Я дуже люблю розробки Python! :-D
MikeyE

Працює, але заморожування здається повільним, особливо якщо ви маєте складну логіку з кількома викликами на поточний час.
Андрій Беляк

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 пояснюється досить заплутано на сайті voidspace.org.uk/python/mock/patch.html#where-to-patch . Здається , що якщо ваш модуль використовує from datetime import dateте ім'я модуля , в якому from datetime import dateі виклик date.today()з'являється
даніо

1
Дякую. Працювали! Приклад: з mock.patch ('testing.views.datetime') як mock_date: mock_date.today.return_value = datetime.datetime (2016, 9, 18) mock_date.side_effect = lambda * args, ** kw: date (* args , ** кВт)
Latrova

36

Я думаю, я прийшов трохи пізно з цього питання, але я думаю, що головна проблема тут полягає в тому, що ви безпосередньо виправляєте datetime.date.today, і, згідно з документацією, це неправильно.

Вам слід виправити посилання, імпортовані у файл, де, наприклад, перевірена функція.

Скажімо, у вас є файл function.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...

Сподіваюсь, це трохи допомагає.


Це виглядає дуже переконливо, але я не можу змусити це запуститись (кидає а NameError: name 'datetime' is not defined). Звідки береться datetime.strptimeпосилання, Mock(return_value=...)якщо ви не імпортуєте datetimeу свій тестовий файл? ОНОВЛЕННЯ: Добре, я просто зайшов і імпортував datetimeмодуль у тестовий файл. Я подумав, що фокус полягає в тому, як ви ховаєте datetimeпосилання з тестового файлу.
імрек

@DrunkenMaster Мені б довелося побачити приклад того, що ти робиш, і яку посилання ти знущався. ти робив import datetimeчи from datetime import strptime? якщо ви робили перший, вам доведеться знущатися datetimeі робити mocked_datetime.strptime.return_value = whatever, це пізніший, вам доведеться безпосередньо висміювати посилання strptime у файлі, де живе перевірений метод.
iferminm

@israelord Що я хотів сказати, це те, що в останньому фрагменті коду (тестовому файлі) відсутній імпорт для посилання на дату, щоб зробити Mock(return_value=datetime...)роботу.
імрек

32

Щоб додати до рішення Даніеля Г :

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)

2
Будьте дуже обережні тут - ви повинні використовувати з версії, інакше ви можете отримати дивацтво, якщо ви використовуєте datetime.date (або datetime або інші). IE - глибина стека, досягнута, коли ваші підроблені нові дзвінки самі.
Danny Staple

У вас не виникне цієї проблеми, якщо підроблений об’єкт знаходиться у власному модулі: 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)

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перестане працювати.


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()розблокувати ^^?
Нам Г ВУ

Ну після спроби використовувати цей макет - один УВАГА для цього рішення всі функціональні можливості з datetime moduleз target_moduleперестане працювати.
Нам Г ВУ

1
Погодьтеся @ frx08, з () зменшить біль. Хоча всередині цього блоку всі, наприклад дата, timedelta припинить роботу. Що робити, якщо нам зараз потрібно глузувати, але математика з датами все ще триває? Вибачте, ми повинні мати .now () глузував не лише з усього модуля datetime.
Нам Г ВУ

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клас на основі С власним підкласом 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__методом.
Dan Loewenherz

Це дійсно працює в Python 2.7, і це вирішило мою проблему з перевіркою типу екземпляра, дякую!
Karatheodory

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


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модуля.


1
Чудова відповідь, більшість тестів, де вам потрібно знущатися над датою, вам потрібно буде використовувати модуль datetime
Antoine Vo

1

Кілька рішень обговорюються в http://blog.xelnor.net/python-mocking-datetime/ . Підсумовуючи:

Об'єкт макету - Простий та ефективний, але порушує перевірку речовини ():

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

0

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

Я думав, що це може допомогти комусь одного дня ...


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 з макетом, ось як я знущався, 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)

По суті, макет повинен бути встановлений для повернення вказаної дати. Ви не в змозі безпосередньо зафіксувати об'єкт дати.


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')

що sourcesу вашому декораторі патчів?
Елена

Шановна @elena, досить важко згадати, про що я думав майже рік тому)). Я думаю, я мав на увазі будь-який модуль наших джерел додатків - лише код вашої програми.
MTMobile

0

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

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

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

Цей код імпортує всі визначення, оптимізовані для С та фактично замінює всі визначення чистого Python. Ми можемо змусити CPython використовувати чисто-Python реалізацію модуля datetime, виконавши:

import datetime
import importlib
import sys

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

Встановивши sys.modules["_datetime"] = None, ми кажемо Python ігнорувати модуль, оптимізований С. Потім ми перезавантажуємо модуль, який спричиняє _datetimeзбій імпорту . Тепер визначення "чистого Python" залишаються і можуть бути зафіксовані нормально.

Якщо ви використовуєте Pytest, тоді додайте фрагмент, наведений вище, у conftest.py, і ви зможете datetimeнормально закріплювати об'єкти.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.