Ложный побочный эффект только X раз

У меня есть задача повторной попытки сельдерея, которую я хотел бы проверить, чтобы она повторялась до тех пор, пока она не будет успешной. Используя mock side_effect, я могу потерпеть неудачу в течение заданного количества выполнений, а затем, передав None, очистить побочный эффект. Однако метод, который вызывает задача, в этот момент не выполняется, просто у него нет исключения. Есть ли способ устранить побочный эффект, и при этом метод, над которым издевались, выполнялся как обычно?

Я могу проверить, что он вызывается «x» несколько раз (т. е. повторять до тех пор, пока он не будет успешным), а затем в отдельном тесте утверждать, что он делает то, что должен, но мне было интересно, есть ли способ сделать оба в одном тесте.

Задачи.py:

import celery

@celery.task(max_retries=None)
def task():
    print "HERE"
    try:
        do_something("TASK")
    except Exception as exc:
        print exc
        raise task.retry(exc=exc)

def do_something(msg):
    print msg

Контрольная работа:

import ....

class TaskTests(test.TestCase):

    @mock.patch('tasks.do_something')
    def test_will_retry_until_successful(self, action):
        action.side_effect = [Exception("First"), Exception("Second"), Exception("Third"), None]
        tasks.task.delay()
        self.assert.... [stuff about task]

Результаты: три раза терпит неудачу, а затем «успешно», но do_something() никогда не печатает. action.call_count равно 4. Я хотел бы видеть, что пустая строка после последнего «ЗДЕСЬ» будет напечатана «ЗАДАЧА».

-------------------- >> begin captured stdout << ---------------------
HERE
First
HERE
Second
HERE
Third
HERE

--------------------- >> end captured stdout << ----------------------

person voidnologo    schedule 18.08.2015    source источник
comment
Ну, вы издевались do_something(). Насмешка над методом не вызывает оригинал без побочных эффектов.   -  person Martijn Pieters    schedule 19.08.2015


Ответы (1)


Вы высмеивали do_something(). Макет полностью заменяет оригинал; ваш выбор состоит в том, чтобы либо иметь побочный эффект (поднять или вернуть значение из итерации), либо применить обычные фиктивные операции (возврат нового фиктивного объекта).

Кроме того, добавление None к последовательности side_effect не сбрасывает побочный эффект, а просто указывает макету вместо этого вернуть значение None. Вместо этого вы можете добавить mock.DEFAULT; в этом случае применяются обычные фиктивные действия (как если бы имитация была вызвана без побочного эффекта):

@mock.patch('tasks.do_something')
def test_will_retry_until_successful(self, action):
    action.side_effect = [Exception("First"), Exception("Second"), Exception("Third"), mock.DEFAULT]
    tasks.task.delay()
    self.assert.... [stuff about task]

Если вы считаете, что ваш тест должен заканчиваться вызовом оригинала, вам придется сохранить ссылку на исходную, не исправленную функцию, а затем установить side_effect в вызываемый объект, который будет разворачиваться и вызывать оригинал, когда приходит время:

# reference to original, global to the test module that won't be patched
from tasks import do_something

class TaskTests(test.TestCase):
    @mock.patch('tasks.do_something')
    def test_will_retry_until_successful(self, action):
        exceptions = iter([Exception("First"), Exception("Second"), Exception("Third")])
        def side_effect(*args, **kwargs):
            try:
                raise next(exceptions)
            except StopIteration:
                # raised all exceptions, call original
                return do_something(*args, **kwargs)
        action.side_effect = side_effect
        tasks.task.delay()
        self.assert.... [stuff about task]

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

person Martijn Pieters    schedule 19.08.2015
comment
Что делать, если do_something является методом класса. Его не так просто сохранить, поскольку это метод класса, и мы каким-то образом должны иметь возможность передать себя. Любые идеи? Кстати, поскольку это какая-то внутренняя логика, ее нельзя просто создать в наборе тестов. - person xiº; 03.04.2017
comment
@xiº: зачем тебе вообще нужен доступ к do_something вообще? Это не часть тестируемого кода, поэтому с точки зрения модульного тестирования вы должны просто издеваться над ним. Однако любую ссылку на вызываемый объект можно сохранить, неважно, является ли он методом класса. - person Martijn Pieters; 03.04.2017
comment
Мне нужно что-то вроде side_effect = [original_valid_call(), original_valid_call(), ValueError]. Но не могу понять, как передать экземпляр в действительном вызове. - person xiº; 03.04.2017
comment
В моем случае do_smth — это просто нижний слой. - person xiº; 03.04.2017
comment
@xiº: почему бы вместо этого не предоставить фиктивные объекты? В противном случае, как бы вы обычно вызывали этот метод класса? - person Martijn Pieters; 03.04.2017
comment
Мне просто нужно оригинальное поведение этого метода несколько раз и исключение на n-м шаге. Это метод класса внутренней библиотеки, мой действительно протестированный код просто беспокоится о том, есть ли какие-либо исключения или нет. - person xiº; 03.04.2017
comment
@xiº: тогда, может быть, ты сможешь опубликовать новый вопрос по этому поводу? В вызове метода класса нет ничего особенного, и я не уверен, в чем ваша проблема. Включите минимальный воспроизводимый пример, чтобы продемонстрировать, где вы застряли. - person Martijn Pieters; 03.04.2017
comment
здесь это stackoverflow.com/ вопросы/43191142/ - person xiº; 03.04.2017