2010-04-17 3 views
18

Когда я пишу с бизнес-логикой, мой код часто зависит от текущего времени. Например, алгоритм, который смотрит на каждый незавершенный порядок и проверяет, следует ли отправлять счет-фактуру (что зависит от количества дней с момента окончания работы). В этих случаях создание счета-фактуры не инициируется явным действием пользователя, а фоновым заданием.Как изменить дату/время в Python для всех модулей?

Теперь это создает проблему для меня, когда дело доходит до тестирования:

  • я могу проверить создание счета-фактуры сам легко
  • Однако трудно создать заказ в тесте и убедитесь, что на фоне работы определяет правильные заказы в правильное время.

До сих пор я нашел два решения:

  • В испытательной установке, рассчитать задание даты по отношению к текущей дате. Недостаток: код становится довольно сложным, поскольку больше нет явных дат. Иногда бизнес-логика довольно сложна для краевых случаев, поэтому становится трудно отлаживаться из-за всех этих относительных дат.
  • У меня есть собственные функции доступа к дате/времени, которые я использую на протяжении всего кода. В тесте я просто установил текущую дату, и все модули получают эту дату. Поэтому я могу смоделировать создание заказа в феврале и проверить, что счет-фактура создается в апреле легко. Даунсайд: сторонние модули не используют этот механизм, поэтому очень сложно интегрировать + протестировать их.

Второй подход был более успешным для меня в конце концов. Поэтому я ищу способ установить время возвращения модулей времени и времени Python. Обычно установить дату достаточно, мне не нужно устанавливать текущий час или секунду (хотя это было бы неплохо).

Есть ли такая утилита? Есть ли (внутренний) Python API, который я могу использовать?

ответ

8

Возможно, исправление обезьян time.time является достаточным, поскольку оно обеспечивает основу почти для всех других временных подпрограмм в Python. Это, похоже, очень хорошо справляется с вашим прецедентом, не прибегая к более сложным трюкам, и не имеет значения, когда вы это делаете (кроме нескольких пакетов stdlib, таких как Queue.py и threading.py, которые делают from time import time, и в этом случае вы должны патч, прежде чем они импортируются):

>>> import datetime 
>>> datetime.datetime.now() 
datetime.datetime(2010, 4, 17, 14, 5, 35, 642000) 
>>> import time 
>>> def mytime(): return 120000000.0 
... 
>>> time.time = mytime 
>>> datetime.datetime.now() 
datetime.datetime(1973, 10, 20, 17, 20) 

Тем не менее, в годы насмешливый объектов для различных типов автоматизированного тестирования, я нужен такой подход очень редко, так как большую часть времени это мой собственный код приложения, нуждается в насмешливых, а не подпрограммах stdlib. В конце концов, вы знаете, что они уже работают. Если вы сталкиваетесь с ситуациями, когда ваш собственный код должен обрабатывать значения, возвращаемые библиотечными процедурами, вы можете захотеть самостоятельно издеваться над библиотечными процедурами, по крайней мере, при проверке того, как ваше собственное приложение будет обрабатывать отметки времени.

Наилучший подход на сегодняшний день состоит в том, чтобы создать собственную процедуру (ы) для обслуживания даты и времени, которую вы используете исключительно в своем коде приложения, и встраивать в нее возможность тестирования для подбора поддельных результатов по мере необходимости. Например, я более сложный эквивалент этого иногда:

# in file apptime.py (for example) 
import time as _time 

class MyTimeService(object): 
    def __init__(self, get_time=None): 
     self.get_time = get_time or _time.time 

    def __call__(self): 
     return self.get_time() 

time = MyTimeService() 

Теперь в моем приложении кода я просто сделать import apptime as time; time.time(), чтобы получить текущее значение времени, в то время как в тестовом коде я сначала сделать apptime.time = MyTimeService(mock_time_func) в моем setUp() коде результаты поддельного времени.

Обновление: Спустя годы есть альтернатива, как указано в Dave Forgac's answer.

+7

Какую версию Python вы проверили, попробуйте это? Я только что пробовал 2,6 на MacOS 10.6, а datetime.datetime.now() блаженно не знает об изменении времени. Time –

+0

Malcolm, я только что тестировал Python 2.7.1 на Windows и работает точно так, как показано выше. , Возможно, это зависит от платформы, и это не удивительно. –

+4

Я тестировал это на Ubuntu 12.04 с Python 2.7.3, и он не работает. После обезглавливания 'time.time' с' mytime', как описано, вызов 'datetime.datetime.now' по-прежнему возвращает текущее время. – djsmith

0

может быть несколько способов сделать это, например, создать заказы (с текущей временной меткой), а затем изменить это значение в БД непосредственно каким-то внешним процессом (при условии, что данные находятся в БД).

Я предложу что-то еще. Вы хотя бы запустили приложение на виртуальной машине, установив время, чтобы сказать Feb, создав заказы, а затем просто изменив время виртуальной машины? Этот подход является самым близким, поскольку вы можете добраться до реальной ситуации.

+0

Да, движущиеся даты в БД непосредственно может работать. Однако это иногда нелегко - особенно у вас много полей даты в разных таблицах (например, у каждого элемента есть отметки времени для создания, изменения + полная история). Таким образом, все это может стать серьезной проблемой. Для виртуальных машин: я не поеду туда, так как это увеличит сложность тестирования на несколько порядков -> тестовая flakiness более вероятна, тесты выполняются медленнее (быстрые тесты чрезвычайно важны для меня!), И для этого требуется чтобы интегрировать это на моих серверах сборки. –

1

Ну один способ сделать это состоит в динамический пластырь модуль времени/дате и время

что-то вроде

import time 
import datetime 

class MyDatetime: 

    def now(self): 
     return time.time() 

datetime.datetime = MyDatetime 

print datetime.datetime().now() 
+1

Да, это может сработать - однако оно все еще слишком хрупко, потому что мне нужно обезвредить все внешние модули: если внешний модуль использует «из datetime import datetime», патч обезьяны выше не будет иметь никакого эффекта. –

+1

@Felix, это будет иметь эффект, если вы исправляете модуль datetime перед импортом чего-либо еще. Если вы планируете делать исправление обезьяны (что не является изящным решением, но может быть прагматичным), тогда вам нужно жить с такой неловкостью. –

2

Вы может пропатчить систему, путем создания модуля пользовательского DATETIME (даже подделка один - см. пример ниже), действующий как прокси-сервер, а затем вставьте его в словарь sys.modules. Оттуда каждый импорт в модуль datetime возвращает ваш прокси.
Существует все еще оговорка класса datetime, особенно когда кто-то делает from datetime import datetime; для этого вы можете просто добавить еще один прокси только для этого класса.

Вот пример того, что я говорю - конечно, это то, что я выбрал за 5 минут, и может иметь несколько проблем (например, тип класса datetime неверен); но, надеюсь, он уже может быть полезен.

import sys 
import datetime as datetime_orig 

class DummyDateTimeModule(sys.__class__): 
    """ Dummy class, for faking datetime module """ 
    def __init__(self): 
    sys.modules["datetime"] = self 
    def __getattr__(self, attr): 
    if attr=="datetime": 
     return DummyDateTimeClass() 
    else: 
     return getattr(datetime_orig, attr) 

class DummyDateTimeClass(object): 
    def __getattr__(self, attr): 
    return getattr(datetime_orig.datetime, attr) 

dt_fake = DummyDateTimeModule() 

И наконец - стоит ли?
Честно говоря, мне нравится наше второе решение гораздо больше, чем это :-).
Да, python - очень динамичный язык, где вы можете делать довольно много интересных вещей, но исправление кода таким образом всегда имеет определенную степень риска, даже если мы говорим здесь о тестовом коде.
Но, в основном, я думаю, что вспомогательная функция сделает более точную проверку теста, а также ваш код будет более явным с точки зрения того, что он будет протестирован, что повысит читаемость.
Поэтому, если изменение не слишком дорого, я бы пошел на второй подход.

4

Пакет freezegun был сделан специально для этой цели. Это позволяет вам изменить дату тестирования кода. Его можно использовать напрямую или через декоратора или менеджера контекста. Один из примеров:

from freezegun import freeze_time 
import datetime 

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

Дополнительные примеры см проекта: https://github.com/spulec/freezegun

+2

Это похоже на лучший вариант в эти дни! К сожалению, проект был создан только на несколько лет слишком поздно, но я буду использовать его в будущем :-) –

+0

Действительно, это, безусловно, лучшее решение. –

Смежные вопросы