Как применить декоратор, не используя его в качестве декоратора?

#python #unit-testing #mocking

#python #модульное тестирование #издевательство

Вопрос:

Я пытаюсь протестировать оформленный метод класса:

 class S3Store(object):
    @retry(exceptions=Exception, delay=1, tries=5, backoff=2)
    def delete(self, dest_id):
        return self._delete(dest_id=dest_id)

    def _delete(self, dest_id):
        bucket = self.conn.get_bucket(get_bucket_from_s3_uri(dest_id))
        key = Key(bucket, get_key_from_s3_uri(dest_id))
        key.delete()
  

Я издевался и тестировал _delete , и теперь я хочу протестировать логику повторных попыток.

Я не могу просто протестировать delete() напрямую, потому что Key это не будет высмеяно. Итак, я надеялся сделать что-то вроде следующего:

 decorated_fn = retry.retry_decorator(storage_backend._delete, delay=0.00001)
storage_backend.delete = decorated_fn
storage_backend.delete(...) ...         # add assertions, etc.
  

Это не работает. Я получаю сообщение об ошибке:

 AttributeError: 'function' object has no attribute 'retry_decorator'
  

Я думаю, проблема в том, что retry декоратор сам оформлен.

Как я могу протестировать логику повторных попыток в моем delete() методе, чтобы его внутренние объекты можно было имитировать, и чтобы время ожидания задержки было очень низким?

Ответ №1:

Вы должны тестировать декоратор повторных попыток не с помощью функции удаления, а с помощью тестовой функции, которая проверяет декоратор повторных попыток.

 def test_retry(self):
    @retry(exceptions=ValueError, delay=1, tries=5, backoff=2)
    def test_raise_wrong_exception():
        raise AssertionError()
    self.assertRaises(AssertionError, test_raise_wrong_exception)
    ...
  

Ответ №2:

Декоратор — это функция, которая принимает функцию в качестве аргумента и возвращает оформленную версию.

Фон

Ваш случай сбивает с толку, потому что он содержит много вложенности. Давайте сначала обновим синтаксис декораторов:

Когда мы пишем:

 @decorator
def fun():
    pass
  

Это эквивалентно:

 def fun():
    pass
fun = decorator(fun)
  

В вашем примере retry функция на самом деле не является декоратором, но создает декоратор:

 @decorator_factory(...)
def fun():
    pass
  

Эквивалентно:

 def fun():
    pass
decorator = decorator_factory(...)
fun = decorator(fun)
  

Решение

Теперь должно быть очевидно, что вы хотите:

 decorator = retry(delay=0.00001)
decorated_fn = decorator(storage_backend._delete)
  

Другое

Если мы посмотрим на исходный код, похоже, что retry_decorator на самом деле это не декоратор: он возвращает результаты f , а не новую функцию с расширенным поведением:

 @decorator
def retry_decorator(f, *args, **kwargs):
    for i in count():
        try:
        return f(*args, **kwargs)
        except exceptions, e:
        if i >= tries:
        raise
        round_delay = delay * backoff ** i
    log.warning('%s, retrying in %s seconds...', e, round_delay)
        time.sleep(round_delay)
  

Но @decorator преобразует retry_decorator в реальный декоратор, смотрите здесь .