How to test a function with input call?
Solution 1
You should probably mock the built-in input
function, you can use the teardown
functionality provided by pytest
to revert back to the original input
function after each test.
import module # The module which contains the call to input
class TestClass:
def test_function_1(self):
# Override the Python built-in input method
module.input = lambda: 'some_input'
# Call the function you would like to test (which uses input)
output = module.function()
assert output == 'expected_output'
def test_function_2(self):
module.input = lambda: 'some_other_input'
output = module.function()
assert output == 'another_expected_output'
def teardown_method(self, method):
# This method is being called after each test case, and it will revert input back to original function
module.input = input
A more elegant solution would be to use the mock
module together with a with statement
. This way you don't need to use teardown and the patched method will only live within the with
scope.
import mock
import module
def test_function():
with mock.patch.object(__builtins__, 'input', lambda: 'some_input'):
assert module.function() == 'expected_output'
Solution 2
As The Compiler suggested, pytest has a new monkeypatch fixture for this. A monkeypatch object can alter an attribute in a class or a value in a dictionary, and then restore its original value at the end of the test.
In this case, the built-in input
function is a value of python's __builtins__
dictionary, so we can alter it like so:
def test_something_that_involves_user_input(monkeypatch):
# monkeypatch the "input" function, so that it returns "Mark".
# This simulates the user entering "Mark" in the terminal:
monkeypatch.setattr('builtins.input', lambda _: "Mark")
# go about using input() like you normally would:
i = input("What is your name?")
assert i == "Mark"
Solution 3
You can replace sys.stdin
with some custom Text IO, like input from a file or an in-memory StringIO buffer:
import sys
class Test:
def test_function(self):
sys.stdin = open("preprogrammed_inputs.txt")
module.call_function()
def setup_method(self):
self.orig_stdin = sys.stdin
def teardown_method(self):
sys.stdin = self.orig_stdin
this is more robust than only patching input()
, as that won't be sufficient if the module uses any other methods of consuming text from stdin.
This can also be done quite elegantly with a custom context manager
import sys
from contextlib import contextmanager
@contextmanager
def replace_stdin(target):
orig = sys.stdin
sys.stdin = target
yield
sys.stdin = orig
And then just use it like this for example:
with replace_stdin(StringIO("some preprogrammed input")):
module.call_function()
Solution 4
This can be done with mock.patch
and with
blocks in python3.
import pytest
import mock
import builtins
"""
The function to test (would usually be loaded
from a module outside this file).
"""
def user_prompt():
ans = input('Enter a number: ')
try:
float(ans)
except:
import sys
sys.exit('NaN')
return 'Your number is {}'.format(ans)
"""
This test will mock input of '19'
"""
def test_user_prompt_ok():
with mock.patch.object(builtins, 'input', lambda _: '19'):
assert user_prompt() == 'Your number is 19'
The line to note is mock.patch.object(builtins, 'input', lambda _: '19'):
, which overrides the input
with the lambda function. Our lambda function takes in a throw-away variable _
because input
takes in an argument.
Here's how you could test the fail case, where user_input calls sys.exit
. The trick here is to get pytest to look for that exception with pytest.raises(SystemExit)
.
"""
This test will mock input of 'nineteen'
"""
def test_user_prompt_exit():
with mock.patch.object(builtins, 'input', lambda _: 'nineteen'):
with pytest.raises(SystemExit):
user_prompt()
You should be able to get this test running by copy and pasting the above code into a file tests/test_.py
and running pytest
from the parent dir.
Solution 5
You can do it with mock.patch
as follows.
First, in your code, create a dummy function for the calls to input
:
def __get_input(text):
return input(text)
In your test functions:
import my_module
from mock import patch
@patch('my_module.__get_input', return_value='y')
def test_what_happens_when_answering_yes(self, mock):
"""
Test what happens when user input is 'y'
"""
# whatever your test function does
For example if you have a loop checking that the only valid answers are in ['y', 'Y', 'n', 'N'] you can test that nothing happens when entering a different value instead.
In this case we assume a
SystemExit
is raised when answering 'N':
@patch('my_module.__get_input')
def test_invalid_answer_remains_in_loop(self, mock):
"""
Test nothing's broken when answer is not ['Y', 'y', 'N', 'n']
"""
with self.assertRaises(SystemExit):
mock.side_effect = ['k', 'l', 'yeah', 'N']
# call to our function asking for input
Zelphir Kaltstahl
Updated on July 05, 2022Comments
-
Zelphir Kaltstahl almost 2 years
I have a console program written in Python. It asks the user questions using the command:
some_input = input('Answer the question:', ...)
How would I test a function containing a call to
input
usingpytest
? I wouldn't want to force a tester to input text many many times only to finish one test run. -
Zelphir Kaltstahl about 8 yearsWould this change the function behind
input
for the whole test session, or only for this one test? -
The Compiler about 8 yearsNo, this would also patch
input
for anything running after that test. You should instead use pytest's monkeypatch fixture to automatically reverse the patching at the end of the test. -
Zelphir Kaltstahl about 8 years@Forge Ah sorry, I was only wondering what question you were referring to as your question. Maybe you posted a similar question or related question somewhere but didn't link it or something.
-
mareoraft about 8 yearsThis is not clear to me. Where is the actual test? When does the teardown_method get called? Perhaps you can put more detail in your answer.
-
Forge about 8 years@mareoraft I've updated my answer to address your questions. I hope it's more clear this way, let me know if you have any questions.
-
Jeff Wright about 4 yearsThanks for this answer. This is precisely what I needed to allow Pytest to run in an environment where I need to query for username/password at the beginning of the test run. All the mock examples above seem to hardcode the mock input into the code itself. That's not a wise thing to do for username/password.
-
mareoraft almost 4 years@cammil Good question! No, it does not. "lambda" is an anonymous function, and it is perfectly fine for a function to accept 0 arguments.
-
cammil almost 4 yearsWhat I mean is, you are passing an argument to
input
, but your lambda does not accept any arguments. -
Hugh over 3 years@cammil the
_
is the argument in this case. Without an argument it would belambda: "Mark"
-
cammil over 3 yearsI had underscore blindness.
-
mareoraft over 3 years@cammil You brought up a very good point. I'm 90% sure that you are correct about lambda needing a parameter in order to accept the input argument. I just don't have the time to verify it myself. And that underscore was added by Giel after you had left your comment. So you are perfectly sane and "insightful".
-
pavol.kutaj about 3 yearsfor this approach but with multiple inputs, see How to simulate two consecutive console inputs with pytest monkeypatch
-
mmi over 2 yearsVery neat way! You don't need to save the original
stdin
, you could just usesys.__stdin___
to restore it. -
Felk over 2 yearsThat is true, but would break down if
sys.stdin
was not set tosys.__stdin___
to begin with. While unlikely, that's theoretically possible and should be accounted for. -
djvg over 2 yearsPytest docs advise against monkey-patching builtin functions. I guess
input
could be an exception here? -
PeptideWitch over 2 yearsJust a heads up on this answer: for Python 3, you'll need to follow the answer below stackoverflow.com/a/55033710/8763097 and specify an input variable for the lambda as well as importing the
builtins
module. Otherwise, you'll get 2 errors: 1 for calling builtins for python 3, and 1 forTypeError: <lambda>() takes 0 positional arguments but 1 was given