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
using pytest
?
I wouldn't want to force a tester to input text many many times only to finish one test run.
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(__builtin__, 'input', lambda: 'some_input'):
assert module.function() == 'expected_output'
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 x: "Mark")
# go about using input() like you normally would:
i = input("What is your name?")
assert i == "Mark"
Edit: Changed lambda: "Mark"
to lambda x: "Mark"
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()
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