4

我试图在我的单元测试中使用 freezegun 来修补数据类中的字段,该字段设置为对象初始化时的当前日期。我想这个问题与任何尝试修补一个在 freezegun 之外用作 default_factory 的函数有关。数据类被冻结,因此它是不可变的。

例如,如果我的数据类是:

@dataclass(frozen=True)
class MyClass:
    name: str
    timestamp: datetime.datetime = field(init=False, default_factory=datetime.datetime.now)

当我用 freezegun 修补 datetime 时,它​​对 MyClass 中时间戳的初始化没有影响(它仍然将时间戳设置为单元测试中 now() 返回的当前日期,导致测试失败)。

我假设它与在补丁到位之前加载默认工厂和模块有关。我尝试修补日期时间,然后使用 importlib.reload 重新加载模块但没有运气。

我目前的解决方案是:

@dataclass(frozen=True)
class MyClass:
    name: str
    timestamp: datetime.datetime = field(init=False)

def __post_init__(self):
   object.__setattr__(self, "timestamp", datetime.datetime.now())

哪个有效。

不过,理想情况下,我想要一个非侵入性的解决方案,它不需要我更改生产代码来启用我的单元测试。

4

1 回答 1

6

你是对的,数据类创建过程在这里做了一些奇怪的事情,这导致了你当前的问题。它在类创建期间绑定工厂函数,这意味着它在 freezegun 有机会修补它之前持有代码的引用。

这是一个没有遇到相同问题的数据类的示例:

from datetime import datetime
from freezegun import freeze_time

class Foo:
  # looks up the function at class creation time
  now_func = datetime.now

  def __init__(self):
    # asks datetime for a reference at instance creation time
    self.timestamp_a = datetime.now()
    # uses an old reference we couldn't patch
    self.timestamp_b = Foo.now_func()


with freeze_time(datetime(2020, 1, 1)):
  foo = Foo()
  assert foo.timestamp_a == datetime(2020, 1, 1)  # works
  assert foo.timestamp_b == datetime(2020, 1, 1)  # raises an AssertionError

至于如何解决这个问题,理论上你可以MyClass.__init__.__closure__在测试期间破解来切换功能,但这有点疯狂。

timestamp在 a中覆盖更好的__post_init__方法可能是只使用 lambda 委托函数调用,以便将名称查找延迟到实例化时间:

timestamp: datetime = field(init=False, default_factory=lambda: datetime.now())

或者您可以开始使用不同的日期时间库,例如 pendulum,它支持开箱即用的冻结时间。FWIW,这就是我最终要做的。

于 2020-04-17T21:31:45.043 回答