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