关于python:试图模拟datetime.date.today(),但不能正常工作

Trying to mock datetime.date.today(), but not working

有人能告诉我为什么这个不起作用吗?

1
2
3
4
5
6
7
8
>>> 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)

也许有人能提出一个更好的方法?


另一个选择是使用https://github.com/spulec/freezegun/

安装:

1
pip install freezegun

并使用它:

1
2
3
4
5
6
7
8
9
10
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

它还影响来自其他模块的方法调用中的其他日期时间调用:

其他模块:

1
2
3
4
from datetime import datetime

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

MY.PY:

1
2
3
4
5
6
7
from freezegun import freeze_time

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

    import other_module
    other_module.other_method()

最后:

1
2
$ python main.py
# 2012-01-01


有一些问题。

首先,你使用mock.patch的方式并不完全正确。当用作修饰器时,它将给定的函数/类(在本例中为datetime.date.today)替换为仅在修饰函数内的Mock对象。所以,只有在你的today()中,datetime.date.today才是一个不同的功能,这似乎不是你想要的。

你真正想要的似乎更像这样:

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

不幸的是,这行不通:

1
2
3
4
5
6
>>> 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并创建正确的函数:

1
2
3
4
5
6
import datetime
class NewDate(datetime.date):
    @classmethod
    def today(cls):
        return cls(2010, 1, 1)
datetime.date = NewDate

现在你可以做:

1
2
>>> datetime.date.today()
NewDate(2010, 1, 1)


为了实现它的价值,模拟文档专门讨论了datetime.date.today,并且可以在不创建虚拟类的情况下进行此操作:

http://www.voidspace.org.uk/python/mock/examples.html部分模拟

1
2
3
4
5
6
7
8
>>> 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)
...


我想我来的有点晚了,但我认为这里的主要问题是您正在直接修补datetime.date.today,根据文档,这是错误的。

例如,您应该修补导入到测试函数所在文件中的引用。

假设您有一个functions.py文件,其中包含以下内容:

1
2
3
4
import datetime

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

那么,在你的测试中,你应该有这样的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
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...

希望这有点帮助。


要添加到Daniel G的解决方案中:

1
2
3
4
5
6
from datetime import date

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

这将创建一个类,该类在实例化时将返回一个普通的datetime.date对象,但也可以更改该对象。

1
2
3
4
5
6
7
@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)


几天前我也遇到了同样的情况,我的解决方案是在模块中定义一个函数来测试并模拟它:

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

今天我发现了Freezegun,它似乎很好地覆盖了这个案子。

1
2
3
4
5
6
7
8
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)


基于Daniel G解决方案,您可以使用以下方法。本机具有不与isinstance(d, datetime.date)断型检查的优点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

基本上,我们用自己的python子类替换基于C的datetime.date类,它生成原始的datetime.date实例,并对isinstance()查询做出与本机datetime.date完全相同的响应。

在测试中用作上下文管理器:

1
2
3
4
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()函数。


对我来说最简单的方法是:

1
2
3
4
5
6
from unittest import patch, Mock

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

此解决方案的注意事项:来自target_module的所有功能将停止工作。


一般来说,您可以将datetimedatetime.date导入某个模块中。模拟该方法的一个更有效的方法是将其修补到导入该方法的模块上。例子:

A.Py

1
2
3
4
from datetime import date

def my_method():
    return date.today()

然后对于您的测试,模拟对象本身将作为参数传递给测试方法。您将使用所需的结果值设置模拟,然后调用被测试的方法。然后您将断言您的方法执行了您想要的操作。

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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这样简单的方法之前,先问问自己是否真的需要模仿它。如果您的测试很短,而且很到位,并且在不模拟函数的情况下工作正常,那么您可能只是在查看正在测试的代码的内部细节,而不是需要模拟的对象。


在http://blog.xelnor.net/python mocking datetime/中讨论了几种解决方案。综上所述:

模拟对象-简单有效但中断isInstance()检查:

1
2
3
4
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())

模拟课堂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)

用作:

1
2
with mock_datetime_now(target, datetime):
   ....

对于那些使用pytest和mocker的人来说,这里是我如何嘲笑datetime.datetime.now(),这与最初的问题非常相似。

1
2
3
4
5
6
7
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)

实际上,必须设置mock以返回指定的日期。您不能直接修补datetime的对象。


这是模拟datetime.date.today()的另一种方法,额外的好处是,由于模拟对象被配置为包装原始datetime模块,其余的datetime功能继续工作:

1
2
3
4
5
6
7
8
9
10
11
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

注意mock.patch()wraps=datetime参数-当foo_module使用除date.today()以外的其他datetime功能时,它们将被转发到原来包装的datetime模块。


在不添加side_effects的情况下,可以模拟来自datetime模块的函数。

1
2
3
4
5
6
7
8
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()

我使用自定义修饰符实现了@user3016183方法:

1
2
3
4
5
6
7
8
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

我想也许有一天能帮到别人…


也许您可以使用自己的"today()"方法,在需要的地方进行修补。模拟utcnow()的例子可以在这里找到:https://bitback.org/k_bx/blog/src/tip/source/en_posts/2012-07-13-double-call-hack.rst?AT =默认值