Как проинструктировать мага c издеваться над тем, как он должен относиться к своим аргументам - PullRequest
1 голос
/ 24 марта 2020

Я столкнулся со следующим (крайним?) Случаем, с которым я не знаю, как правильно обращаться. Общая проблема заключается в том, что

  • У меня есть функция, которую я хочу проверить
  • , в этой функции я вызываю внешнюю функцию с генератором в качестве аргумента
  • в моих тестах я высмеиваю внешнюю функцию
  • , теперь код prod и тестируемый код различаются: в prod генератор используется, mock не делает этого

Вот сокращенный пример того, как это выглядит в моей кодовой базе:

import itertools
import random


def my_side_effects():
    # imaginge itertools.accumulate was some expensive strange function
    # that consumes an iterable
    itertools.accumulate(random.randint(1, 5) for _ in range(10))


def test_my_side_effects(mocker):
    my_mocked_func = mocker.patch('itertools.accumulate')

    my_side_effects()

    # make sure that side-effects took place. can't do much else.
    assert my_mocked_func.call_count == 1

Тест работает очень хорошо и достаточно хорош для всех, кого я забочусь. Но когда я запускаю coverage в коде, ситуация, которую я описал в аннотации, становится очевидной:

----------- coverage: platform linux, python 3.8.0-final-0 -----------
Name                                   Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------------------------------------
[...]
my_test_case.py                            5      0      2      1    86%   6->exit
[...]
----------------------------------------------------------------------------------
# something like this, the ->exit part on the external call is the relevant part

Объяснение синтаксиса ->exit в покрытии. py. Учитывая, что понимание может выполнить соответствующие бизнес-логики c, которые я действительно хочу запустить, пропущенное покрытие имеет значение. Здесь просто вызывается random.randint, но он может сделать что-либо.


Обходные пути:

  1. Вместо этого я могу просто использовать понимание списка. Код называется, и все счастливы. Кроме меня, который должен изменить свой бэкэнд для того, чтобы смягчить тесты.
  2. Я могу добраться до макета во время теста, взять аргумент вызова и развернуть его вручную. Это, вероятно, будет выглядеть чудовищно.
  3. Я могу обезопасить функцию вместо использования волшебной шашки, что-то вроде monkeypatch.setattr('itertools.accumulate', lambda x: [*x]) было бы довольно наглядно. Но я бы потерял способность делать утверждения вызовов, как в моем примере.

То, что я считаю хорошим решением, было бы что-то вроде этого, которого, к сожалению, не существует:

def test_my_side_effects(mocker):
    my_mocked_func = mocker.patch('itertools.accumulate')

    # could also take "await", and assign treatments by keyword
    my_mocked_func.arg_treatment('unroll')  

    my_side_effects()

    # make sure that side-effects took place. can't do much else.
    assert my_mocked_func.call_count == 1

Ответы [ 2 ]

2 голосов
/ 25 марта 2020

Вы правы, что здесь отсутствует покрытие: фактически, поскольку накопление никогда не использовалось, вы могли бы даже иметь:

itertools.accumulate(ERRORERRORERROR for _ in range(10))

И ваш существующий тест все равно пройдет (очевидная ошибка только что получена издевались).

Чтобы решить эту проблему, используйте side_effect из макета:

my_mocked_func = mocker.patch('itertools.accumulate', side_effect=list)

При использовании вызываемого в качестве макета side_effect, он вызывается с теми же аргументами, что и макет, и возвращаемое значение этого вызываемого объекта используется как возвращаемое значение макета (примечание: это означает, что вы можете также утверждать возвращаемое значение здесь, а не только тупое утверждение call_count) .

Это позволит вам поглотить генератор и получить 100% охват здесь.

1 голос
/ 24 марта 2020

Делаем это по-старому:

import itertools

def func():
    return list(itertools.izip(["a", "b", "c"], [1, 2, 3]))

def test_mock():
    callargs = []
    def mock_zip(*args):
        callargs.append(args)
        for arg in args:
            list(arg)
        yield ("a", 1)
        yield ("b", 2)

    old_izip = itertools.izip
    itertools.izip = mock_zip

    result = func()

    itertools.izip = old_izip

    assert 1 == len(callargs), "oops, not called once"
    assert result == [("a", 1), ("b", 2)], "oops, wrong result"

    print("success")
...