End goal: I want to be able to quickly mock the input()
built-in function in pytest, and replace it with an iterator that generates a (variable) list of strings. This is my current version, which works:
from typing import Callable
import pytest
def _create_patched_input(str_list: list[str]) -> Callable:
str_iter = iter(str_list.copy())
def patched_input(prompt: str) -> str: # has the same signature as input
val = next(str_iter)
print(prompt + val, end="\n"),
return val
return patched_input
@pytest.fixture
def _mock_input(monkeypatch, input_string_list: list[str]):
patched_input = _create_patched_input(input_string_list)
monkeypatch.setattr("builtins.input", patched_input)
def mock_input(f):
return pytest.mark.usefixtures("_mock_input")(f)
# Beginning of test code
def get_name(prompt: str) -> str:
return input(prompt)
@mock_input
@pytest.mark.parametrize(
"input_string_list",
(["Alice", "Bob", "Carol"], ["Dale", "Evie", "Frank", "George"]),
)
def test_get_name(input_string_list):
for name in input_string_list:
assert get_name("What is your name?") == name
However, this feels incomplete for a few reasons:
parameterize
call to be input_string_list
, which feels brittle.mock_input
and _mock_input
.What would feel correct to me is to have a decorator (factory) that can be used like @mock_input(strings)
, such that you could use it like
@mock_input(["Alice", "Bob", "Carol"])
def test_get_name():
....
or, more in line with my use case,
@pytest.mark.parametrize(
"input_list", # can be named whatever
(["Alice", "Bob", "Carol"], ["Dale", "Evie", "Frank", "George"]),
)
@mock_input(input_list)
def test_get_name():
....
The latter I don't think you can do, as pytest wont recognize it as a fixture. What's the best way to do this?
I'd use indirect parametrization for mock_input
, since it cannot work without receiving parameters. Also, I would refactor mock_input
into a fixture that does passing through the arguments it receives, performing the mocking on the way. For example, when using unittest.mock.patch()
:
import pytest
from unittest.mock import patch
@pytest.fixture
def inputs(request):
texts = requests.param # ["Alice", "Bob", "Carol"] etc
with patch('builtins.input', side_effect=texts):
yield texts
Or, if you want to use monkeypatch
, the code gets a bit more complex:
@pytest.fixture
def inputs(monkeypatch, request):
texts = requests.param
it = iter(texts)
def fake_input(prefix):
return next(it)
monkeypatch.setattr('builtins.input', fake_input)
yield texts
Now use inputs
as test argument and parametrize it indirectly:
@pytest.mark.parametrize(
'inputs',
(["Alice", "Bob", "Carol"], ["Dale", "Evie", "Frank", "George"]),
indirect=True
)
def test_get_name(inputs):
for name in inputs:
assert get_name("What is your name?") == name