尝试模拟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)

也许有人可以提出更好的方法?



Answers:


124

有一些问题。

首先,您使用的方式mock.patch不太正确。当用作装饰器时,它仅在装饰函数内datetime.date.todayMock对象替换给定的函数/类(在这种情况下为)。因此,只有在您将是一个不同的功能,不会出现你想要的。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
尽管这个答案很不错,但可以模拟日期时间而不创建类:stackoverflow.com/a/25652721/117268
EmilStenström2015年

您如何将datetime实例恢复为其原始值?与deepcoppy
Oleg Belousov

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))))
Jonhy Beebop,

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
一个非常有用的图书馆
Shaun

3
如果您注意到您的freezegun测试运行缓慢,您也可以尝试使用python-libfaketime
西蒙·韦伯

很棒的库,但不幸的是,它无法与Google App Engine NDB /数据存储区配合使用。
布兰登

我喜欢“ freezegun”是图书馆的名字。我真的很喜欢Python开发人员!:-D
MikeyE

可以,但是freezegun似乎很慢,尤其是当您具有复杂的逻辑并且需要多次调用当前时间时。
Andrey Belyak

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
这对我真的没有用。尽管我很欣赏在查找条目时所付出的努力。
Pradyot,2015年

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
谢谢。工作了!示例:将嘲讽.match.patch('tests.views.datetime')设为嘲讽日期:mock_date.today.return_value = datetime.datetime(2016、9、18)mock_date.side_effect = lambda * args,** kw:date(* args ,** kw)
Latrova

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

希望这会有所帮助。


这看起来非常引人注目,但我无法运行它(引发NameError: name 'datetime' is not defined)。如果您不导入测试文件,则datetime.strptime引用Mock(return_value=...)来自何处datetime?更新:没关系,我只是继续将datetime模块导入测试文件中。我以为诀窍在于如何datetime从测试文件中隐藏引用。
imrek

@DrunkenMaster我必须看到一个示例,说明您在做什么以及您在嘲笑哪个引用。你在干什么import datetime还是from datetime import strptime?如果您正在做第一个,则必须进行模拟,datetime而do mocked_datetime.strptime.return_value = whatever是后一个,则必须直接在测试方法所在的文件中模拟strptime引用。
iferminm

@israelord我的意思是,您的最后一个代码段(测试文件)缺少导入日期以进行Mock(return_value=datetime...)工作的datetime引用。
imrek

32

要添加到Daniel 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)

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)

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 moduletarget_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()不受嘲笑的^^?
Nam G VU

试图利用这个模拟后嘛-一个注意此解决方案是从所有的功能datetime moduletarget_module停止工作。
Nam G VU

1
同意@ frx08 with()会减轻痛苦。尽管在该块内,例如日期,timedelta将停止工作。如果我们现在需要嘲笑但日期数学仍然继续怎么办?抱歉,我们必须仅模拟.now()而不模拟整个datetime模块。
南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自己的python子类替换基于C的类,该子类产生原始datetime.date实例并isinstance()完全像native一样响应查询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。我正在使用该__instancecheck__方法获得最大递归深度RuntimeError 。
Dan Loewenherz 2014年

这确实在Python 2.7中有效,并且解决了实例类型检查的问题,谢谢!
Karatheodory

4

一般来说,您可能已经datetime或可能将其datetime.date导入到某个模块中。模拟该方法的一种更有效的方法是将其修补在要导入的模块上。例:

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/中讨论了几种解决方案。综上所述:

模拟对象 -简单高效,但中断了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):
   ....


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)

本质上,模拟必须设置为返回指定的日期。您无法直接修补datetime的对象。


0

我通过导入datetimeas 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

亲爱的@elena,很难记得我差不多一年前的想法)。我想我的意思仅仅是我们应用程序源的任何模块-只是您应用程序的代码。
MTMobile

0

CPython实际上使用纯Python Lib / datetime.py和C优化的模块/_datetimemodule.c来实现datetime模块。C最佳化版本无法修补,而纯Python版本可以修补。

Lib / datetime.py中的纯Python实现的底部是以下代码:

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

此代码导入所有C优化的定义,并有效替换所有纯Python定义。我们可以通过以下操作强制CPython使用datetime模块的纯Python实现:

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正常修补对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.