Python Классы данных: высмеивание фабрики по умолчанию в замороженном классе данных - PullRequest
3 голосов
/ 16 апреля 2020

Я пытаюсь использовать freezegun в своих модульных тестах, чтобы исправить поле в классе данных, в котором установлена ​​текущая дата инициализации объекта. Я полагаю, что этот вопрос относится к любой попытке исправить функцию, используемую как default_factory, за пределами только freezegun. Класс данных заморожен, поэтому он неизменен.

Например, если мой класс данных:

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

Когда я исправляю дату и время с помощью freezegun, это не влияет на инициализацию метки времени в MyClass ( он по-прежнему устанавливает отметку времени для текущей даты, возвращаемой now () в модульном тесте, что приводит к сбою теста).

Я предполагаю, что это связано с заводской настройкой по умолчанию и загрузкой модуля задолго до установки патча. Я попытался пропатчить datetime, а затем перезагрузить модуль с помощью 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())

, которое работает.

В идеале мне бы хотелось неинвазивное решение, которое не требовало бы от меня изменения производственного кода для включения модульных тестов.

1 Ответ

4 голосов
/ 18 апреля 2020

Вы правы, процесс создания класса данных делает здесь что-то странное, что приводит к вашей текущей проблеме. Он связывает фабричную функцию во время создания класса, что означает, что он содержит ссылку на код до того, как 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 в __post_init__, может заключаться в простом делегировании вызова функции с лямбда-выражением, так что поиск имени задерживается на время создания экземпляра:

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

Или вы можете начать использовать другую библиотеку даты и времени, такую ​​как маятник, которая поддерживает время замораживания из коробки . FWIW, это то, что я в итоге сделал.

...