How to test a function with input call?
PythonUnit TestingTestingInputPytestPython Problem Overview
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.
Python Solutions
Solution 1 - Python
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 2 - Python
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 3 - Python
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 - Python
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 - Python
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
Solution 6 - Python
Since I need the input() call to pause and check my hardware status LEDs, I had to deal with the situation without mocking. I used the -s flag.
python -m pytest -s test_LEDs.py
The -s flag essentially means: shortcut for --capture=no.
Solution 7 - Python
You can also use environment variables in your test code. For example if you want to give path as argument you can read env variable and set default value if it's missing.
import os
...
input = os.getenv('INPUT', default='inputDefault/')
Then start with default argument
pytest ./mytest.py
or with custom argument
INPUT=newInput/ pytest ./mytest.py
Solution 8 - Python
A different alternative that does not require using a lambda function and provides more control during the tests is to use the mock
decorator from the standard unittest
module.
It also has the additional advantage of patching just where the object (i.e. input
) is looked up, which is the recommended strategy.
# path/to/test/module.py
def my_func():
some_input = input('Answer the question:')
return some_input
# tests/my_tests.py
from unittest import mock
from path.to.test.module import my_func
@mock.patch("path.to.test.module.input")
def test_something_that_involves_user_input(mock_input):
mock_input.return_value = "This is my answer!"
assert my_func() == "This is my answer!"
mock_input.assert_called_once() # Optionally check one and only one call