Python 数据类:模拟冻结数据类中的默认工厂

Python Dataclasses: Mocking the default factory in a frozen Dataclass

我试图在我的单元测试中使用 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())

有效。

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

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

这是一个没有数据的例子classes 遇到同样的问题:

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__来切换功能,但这有点疯狂。

仍然比在 __post_init__ 中覆盖 timestamp 好一点的可能是仅使用 lambda 委托函数调用,以便将名称查找延迟到实例化时间:

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

您可以开始使用不同的日期时间库,例如支持冻结时间的 pendulum out of the box。 FWIW,这就是我最终做的。