如何在一个类方法中模拟Python的datetime.now()以进行单元测试?

53

我正在尝试为一个具有以下方法的类编写测试:

import datetime
import pytz

class MyClass:
    def get_now(self, timezone):
        return datetime.datetime.now(timezone)

    def do_many_things(self, tz_string='Europe/London'):
        tz = pytz.timezone(tz_string)
        localtime_now = self.get_now(tz)
        ...
        return things

我想要测试它,为此我需要确保 datetime.datetime.now() 调用返回的时间是可预测的。

我阅读了许多使用Mock进行测试的示例,但没有找到与我需要的完全相似的内容,并且我无法弄清楚如何在我的测试中使用它。

我将get_now()方法分离出来,以便更容易模拟它,而不是datetime.datetime.now(),但我仍然被卡住了。有关如何使用 Mock 编写此单元测试的任何想法吗?(这全部都是在 Django 中进行的,FWIW;我不确定这在本例中是否有所不同。)


仅供参考,永远不要在datetime构造函数中使用pytz时区。请改用“localize”。 - Mark Ransom
谢谢Mark。所以我应该使用timezone.localize(datetime.datetime.now())而不是datetime.datetime.now(timezone)?有什么特别的原因吗? - Phil Gyford
有时直接分配时区不起作用。例如,请参见https://dev59.com/4mnWa4cB1Zd3GeqP0GV8 - Mark Ransom
尽管 localize() 仅用于简单的“naive datetimes”。因此,如果 now() 是 2012-10-26 15:00:00(没有时区),那么它只是将指定的时区应用于它;它不会转换时间。由于我想在特定时区获取实际时间,所以我认为需要执行以下操作:datetime.datetime.now(pytz.utc).astimezone(timezone) - Phil Gyford
8个回答

46

你可以使用freezegun

from freezegun import freeze_time

def test():
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)
    with freeze_time("2012-01-14"):
        assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)

它基本上是在嘲笑 datetime 模块的调用。


1
谢谢,它有效!然而第一个断言失败了,因为月份标记在这个格式中不被接受:01,但是datetime.datetime(2012, 1, 14)可以工作。 - Montaro
1
freezegun很慢,特别是当你使用多个调用datetime.now()测试逻辑时。 - Andrey Belyak

34

您需要创建一个函数,该函数返回特定的日期时间,并根据传入的时区进行本地化:

import mock

def mocked_get_now(timezone):
    dt = datetime.datetime(2012, 1, 1, 10, 10, 10)
    return timezone.localize(dt)

@mock.patch('path.to.your.models.MyClass.get_now', side_effect=mocked_get_now)
def your_test(self, mock_obj):
    # Within this test, `MyClass.get_now()` is a mock that'll return a predictable
    # timezone-aware datetime object, set to 2012-01-01 10:10:10.

这样,您就可以测试结果为时区感知的日期时间是否被正确处理;其他地方的结果应该显示正确的时区,但日期和时间将是可预测的。

在模拟get_now时,您使用mocked_get_now函数作为副作用;每当代码调用get_now时,调用会被记录下来,并且mocked_get_now会被调用,其返回值被用作返回给get_now调用者的值。


@MartijnPieters 无法导入 mock - Avinash Raj
@AvinashRaj mock是一个附加包,可以使用pip进行安装。在Python 3中,它被包含在unittest.mock中。 - Martijn Pieters

8

这是我认为最优雅的方法:

import datetime
from unittest import mock

test_now = datetime.datetime(1856, 7, 10)
with mock.patch('datetime.datetime', wraps=datetime.datetime) as dt:
    print(dt.now()) # calls the real thing
    dt.now.return_value = test_now
    print(dt.now()) # calls the mocked value

这里的优点是你不需要通过被测试模块的本地属性来打补丁 datetime 模块,它支持调用未mock的方法,并且不需要任何外部导入。

5

我正在使用date,但是同样的想法也适用于datetime

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

...

from mock import patch

@patch('some.module.date', SpoofDate)
def testSomething(self):
    SpoofDate.today = classmethod(lambda cls : date(2012, 9, 24))

some.module 在导入 date 模块。Patch 将导入的 date 替换为 SpoofDate,然后您可以重新定义它以执行任何操作。


@FearlessFuture 你刚刚是不是把 date 替换成了 datetime?你能否用 datetime 写出实现代码。 - zakiakhmad
@zakiakhmad,以下是我所做的一个示例:class StubDate(datetime.datetime): pass @mock.patch("friend.datetime.datetime", StubDate) def test_generate_date(self): # 使 datetime.datetime.now 返回一个固定值 StubDate.now = classmethod(lambda cls: datetime.datetime(2015, 03, 11, 11, 01)) self.assertEqual( self.friend_obj.generate_date(input), datetime.datetime(2015, 03, 11, 11, 01)) > Blockquote - FearlessFuture
@FearlessFuture 非常感谢!我在这里写了一个要点,以便更好地阅读。https://gist.github.com/za/2a217c47582737f88259 - zakiakhmad

3

在最初提出这个问题后...

正如@Jocelyn delalande所建议的那样,我一直很高兴地使用freezegun几年了。

另一个选择是python-libfaketime,它比freezegun快得多,但不适用于Windows并且可能有点麻烦。

一个更新的选择是time-machine,在这篇博客文章中介绍了这三个选项的比较。


2
使用unittest.mock的补丁。
from unittest.mock import patch

@patch('MyClass.datetime')
def test_foo(self, mock_datetime):
    mock_datetime.datetime.now.return_value = datetime.datetime(2019, 5, 7) #SOME_MOCKED_DATE

请注意,我们正在覆盖仅在我们的类中导入的datetime模块。
我们正在编写测试的类:
import datetime

class MyClass:
    def foo():
       localtime_now = datetime.datetime.now(timezone)

我们不需要将get_now()方法分离出来,只是为了更容易进行模拟。

2

1
谢谢,这比任何其他解决方案都更好、更简单。 - Andrey Belyak

1
如果您不想安装任何东西,这是最简单的方法。只需使用Mock类 -
class NewDt(datetime.date):
    @classmethod
     def now(cls):
           return datetime.datetime.strptime('2020-07-10 05:20:20', '%Y-%m-%d %H:%M:%S')

在模拟函数之前使用这个补丁。
 @mock.patch('module path', NewDt)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接