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

时间:2010-12-19 06:54:39

标签: python testing datetime mocking

有谁能告诉我为什么这不起作用?

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

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

20 个答案:

答案 0 :(得分:130)

另一种选择是使用 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

答案 1 :(得分:102)

有一些问题。

首先,您使用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'

不幸的是,这不起作用:

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

由于Python内置类型是不可变的,因此失败 - 有关更多详细信息,请参阅this answer

在这种情况下,我会自己继承datetime.date并创建正确的函数:

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

现在你可以这样做:

{{1}}

答案 2 :(得分:84)

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

http://www.voidspace.org.uk/python/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)
...

答案 3 :(得分:31)

我想我来得有点迟了但我认为这里的主要问题是你直接修补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...

希望这有点帮助。

答案 4 :(得分:29)

添加到Daniel G's solution

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)

答案 5 :(得分:5)

我几天前遇到了同样的情况,我的解决方案是在模块中定义一个函数进行测试,然后嘲笑:

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)

答案 6 :(得分:5)

您可以使用以下方法,基于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)

基本上,我们将基于C的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()函数。

答案 7 :(得分:4)

对我来说最简单的方法就是这样做:

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

此解决方案的小心:来自datetime module的{​​{1}}的所有功能都将停止工作。

答案 8 :(得分:3)

一般来说,您可能会将datetimedatetime.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这样简单的方法之前,问问自己是否真的需要来模拟它。如果您的测试很简短且没有嘲笑函数,那么您可能只是查看正在测试的代码的内部细节,而不是需要模拟的对象。

答案 9 :(得分:2)

我们可以使用pytest-mock(https://pypi.org/project/pytest-mock/)模拟对象来模拟特定模块中的日期时间行为

假设您要在以下文件中模拟日期时间

# File path - source_dir/x/a.py
import datetime

def name_function():
     name = datetime.now()
     return f"name_{name}"

在测试功能中,测试运行时会将嘲笑者添加到该功能中

def test_name_function(mocker):
     mocker.patch('x.a.datetime')
     x.a.datetime.now.return_value = datetime(2019, 1, 1)

     actual = name_function()

     assert actual == "name_2019-01-01"

答案 10 :(得分: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):
   ....

答案 11 :(得分:1)

对我来说最好的方法是结合@Daniel G 和@frx08 解决方案:

class Test_mock_date:
    class NewDate(datetime.datetime):
        @classmethod
        def now(cls, tz=None):
            return cls(2021, 5, 12)

    def test_mock_date(self):
        with patch('datetime.datetime', new = self.NewDate):
            assert datetime.datetime.now() == datetime.datetime(2021, 5, 12, 0, 0)

现在有一个奇怪的副作用或日期时间覆盖,希望它有所帮助!

答案 12 :(得分: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

我认为有一天这可能对某人有所帮助......

答案 13 :(得分:0)

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

答案 14 :(得分:0)

可以从* { min-height: 0; min-width: 0; } 模块模拟函数,而无需添加datetime

side_effects

答案 15 :(得分:0)

这是模拟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的{​​{1}}参数–当mock.patch()使用除foo_module之外的其他datetime函数时,它们将被转发到原始包装的{{1 }}模块。

答案 16 :(得分: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)

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

答案 17 :(得分: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)

答案 18 :(得分: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')

答案 19 :(得分:0)

CPython实际上使用纯Python Lib/datetime.py和经过C优化的Modules/_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对象。