使用Freezegun冻结时区感知datetime仍触发RuntimeWarning的解决方法
问题场景
你在开发机构暂停功能的测试用例时,使用freezegun冻结了带UTC时区的datetime.datetime对象,打印验证时能看到时区感知的时间(2000-01-01 00:00:00+00:00),但运行测试时一直收到Django的RuntimeWarning:
/usr/local/lib/python2.7/dist-packages/django/db/models/fields/init.py:1447: RuntimeWarning: DateTimeField Organization.suspended received a naive datetime (2000-01-01 00:00:00) while time zone support is active. RuntimeWarning)
你的测试代码如下:
import datetime import pytz from freezegun import freeze_time # crop class OrganizationSuspendTestCase(TestCase): def setUp(self): self.organization = OrganizationFactory() self.fake_datetime = datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=pytz.UTC) print self.fake_datetime def test_suspend_organization(self): """ Case: An organization is being suspended with the suspend service. Expected: The organization is updated with the suspending datetime and the reason. """ with freeze_time(self.fake_datetime): mc_organization.services.organization.suspend(organization=self.organization, reason="Did not pay the bill") self.assertEqual(self.organization.suspended, datetime.datetime(2000, 1, 1, 0, 0, 0))
问题根源
其实问题出在业务服务代码而非测试代码:你的测试用例正确冻结了带时区的时间,但服务内部原本使用datetime.datetime.now()生成的是无时区感知(naive)的时间对象——哪怕freezegun冻结了时间,datetime.datetime.now()返回的依然是不带时区的结果,而Django开启时区支持后,不允许将naive时间存入DateTimeField,因此触发了警告。
解决方案
将服务代码中获取当前时间的方式替换为Django提供的timezone.now(),这个方法会返回时区感知的时间对象,能和freezegun冻结的带时区时间兼容。修改后的服务代码如下:
def suspend(organization, reason): """ Suspend an organization with the given reason. :param mc_organization.models.Organization organization: The organization to suspend. :param string reason: The reason of the suspending. :return: None """ # organization.suspended = datetime.datetime.now() (Old situation) organization.suspended = timezone.now() # timezone aware situation. organization.suspended_reason = reason organization.save()
另外建议同步调整测试中的断言,使用带时区的时间对象,保持代码一致性:
self.assertEqual(self.organization.suspended, datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=pytz.UTC))
内容的提问来源于stack exchange,提问作者Nrzonline

