82

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.

codeforester
  • 39,467
  • 16
  • 112
  • 140
Zelphir Kaltstahl
  • 5,722
  • 10
  • 57
  • 86
  • 1
    @idjaw Not recently. I used pytest before, but this came to my mind when thinking about doing TDD for my project here and I have no idea how to solve it. I'll take a look at those tuts again. – Zelphir Kaltstahl Mar 07 '16 at 18:39
  • In your test function, you could reassign the `input()` function to something else (also known as "monkey patching" or "shadowing"). – John Gordon Mar 07 '16 at 18:47
  • @JohnGordon Not a bad idea, that might be the way. – Zelphir Kaltstahl Mar 07 '16 at 19:47
  • Related (non duplicate): http://stackoverflow.com/questions/6271947/how-can-i-simulate-input-to-stdin-for-pyunit – Tersosauros Apr 04 '16 at 08:50
  • @ZelphirKaltstahl perhaps you should change the accepted answer to my answer below, as it is simpler (requires no teardown) and has more votes. – mareoraft Sep 09 '19 at 15:40
  • @mareoraft I hesitate to do that for several reasons. (1) The accepted answer is a valid and useful answer, which was there when I needed it and it did not become a wrong answer. (2) The accepted answer is simple to understand and not overly complicated. (3) The accepted answer works for more versions of PyTest. (4) Your answer contains a reference to a 404 (the accepted answer as well though). (5) The accepted answer considers the fact, that I was asking for testing a function _containing_ a call to `input` and it is visible in the code of the test. I upvoted you anyway. – Zelphir Kaltstahl Sep 12 '19 at 21:28

10 Answers10

81

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"
Giel
  • 2,787
  • 2
  • 20
  • 25
mareoraft
  • 3,474
  • 4
  • 26
  • 62
  • @cammil Good question! No, it does not. "lambda" is an anonymous function, and it is perfectly fine for a function to accept 0 arguments. – mareoraft Aug 03 '20 at 13:56
  • 5
    What I mean is, you are passing an argument to `input`, but your lambda does not accept any arguments. – cammil Aug 03 '20 at 21:15
  • 6
    @cammil the `_` is the argument in this case. Without an argument it would be `lambda: "Mark"` – Hugh Aug 31 '20 at 02:07
  • 2
    I had underscore blindness. – cammil Aug 31 '20 at 07:27
  • 1
    @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". – mareoraft Sep 06 '20 at 20:47
  • 1
    for this approach but with multiple inputs, see [How to simulate two consecutive console inputs with pytest monkeypatch](https://stackoverflow.com/questions/59986625/how-to-simulate-two-consecutive-console-inputs-with-pytest-monkeypatch) – pavol.kutaj Mar 18 '21 at 09:28
  • Pytest docs [advise against monkey-patching builtin functions](https://docs.pytest.org/en/latest/how-to/monkeypatch.html#global-patch-example-preventing-requests-from-remote-operations). I guess `input` could be an exception here? – djvg Dec 09 '21 at 08:24
34

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'
Forge
  • 6,538
  • 6
  • 44
  • 64
  • Would this change the function behind `input` for the whole test session, or only for this one test? – Zelphir Kaltstahl Mar 07 '16 at 19:48
  • 7
    No, this would also patch `input` for anything running after that test. You should instead use pytest's [monkeypatch fixture](http://pytest.org/latest/monkeypatch.html) to automatically reverse the patching at the end of the test. – The Compiler Mar 08 '16 at 05:13
  • @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. – Zelphir Kaltstahl Mar 09 '16 at 18:16
  • This 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. – mareoraft Apr 02 '16 at 15:38
  • @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. – Forge Apr 09 '16 at 15:52
  • Just a heads up on this answer: for Python 3, you'll need to follow the answer below https://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 for `TypeError: () takes 0 positional arguments but 1 was given` – PeptideWitch Jan 21 '22 at 05:59
33

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()
Felk
  • 7,720
  • 2
  • 35
  • 65
  • Very neat way! You don't need to save the original ```stdin```, you could just use ```sys.__stdin___``` to restore it. – mmi Aug 22 '21 at 14:47
  • That is true, but would break down if `sys.stdin` was not set to `sys.__stdin___` to begin with. While unlikely, that's theoretically possible and should be accounted for. – Felk Aug 22 '21 at 14:49
10

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.

Alex
  • 12,078
  • 6
  • 64
  • 74
5

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.

Arindam Roychowdhury
  • 5,927
  • 5
  • 55
  • 63
  • 2
    Thanks 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. – Jeff Wright May 11 '20 at 14:15
3

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
rtaft
  • 2,139
  • 1
  • 16
  • 32
fernandezcuesta
  • 2,390
  • 1
  • 15
  • 32
2

I don't have enough points to comment, but this answer: https://stackoverflow.com/a/55033710/10420225 doesn't work if you just copy/pasta.

Part One

For Python3, import mock doesn't work.

You need import unittest.mock and call it as unittest.mock.patch.object(), or from unittest import mock mock.patch.object()...

If using Python3.3+ the above should "just work". If using Python3.3- you need to pip install mock. See this answer for more info: https://stackoverflow.com/a/11501626/10420225

Part Two

Also, if you want to make this example more realistic, i.e. importing the function from outside the file and using it, there's more assembly required.

This is general directory structure we'll use

root/
  src/prompt_user.py
  tests/test_prompt_user.py

If function in external file

# /root/src/prompt_user.py
def user_prompt():
    ans = input("Enter a number: ")
    try:
        float(ans)
    except:
        import sys

        sys.exit("NaN")
    return "Your number is {}".format(ans)
# /root/tests/test_prompt_user.py
import pytest
from unittest import mock
import builtins

from prompt_user import user_prompt

def test_user_prompt_ok():
    with mock.patch.object(builtins, "input", lambda _: "19"):
        assert user_prompt() == "Your number is 19"

If function in a class in external file

# /root/src/prompt_user.py
class Prompt:
    def user_prompt(self):
        ans = input("Enter a number: ")
        try:
            float(ans)
        except:
            import sys

            sys.exit("NaN")
        return "Your number is {}".format(ans)
# /root/tests/test_prompt_user.py
import pytest
from unittest import mock
import builtins

from mocking_test import Prompt


def test_user_prompt_ok():
    with mock.patch.object(builtins, "input", lambda _: "19"):
        assert Prompt.user_prompt(Prompt) == "Your number is 19"

Hopefully this helps people a bit more. I find these very simple examples almost useless because it leaves a lot out for real world use cases.

Edit: If you run into pytest import issues when running from external files, I would recommend looking over this answer: PATH issue with pytest 'ImportError: No module named YadaYadaYada'

1

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   
swimmer
  • 1,971
  • 2
  • 17
  • 28
1

The simplest way that works without mocking and easily in doctest for lightweight testing, is just making the input_function a parameter to your function and passing in this FakeInput class with the appropriate list of inputs that you want:

class FakeInput:
    def __init__(self, input):
        self.input = input
        self.index = 0

    def __call__(self):
        line = self.input[self.index % len(self.input)]
        self.index += 1
        return line

Here is an example usage to test some functions using the input function:

import doctest

class FakeInput:
    def __init__(self, input):
        self.input = input
        self.index = 0

    def __call__(self):
        line = self.input[self.index % len(self.input)]
        self.index += 1
        return line
    
def add_one_to_input(input_func=input):
    """
    >>> add_one_to_input(FakeInput(['1']))
    2
    """
    return int(input_func()) + 1

def add_inputs(input_func=input):
    """
    >>> add_inputs(FakeInput(['1', '5']))
    6
    """
    return int(input_func()) + int(input_func())

def return_ten_inputs(input_func=input):
    """
    >>> return_ten_inputs(FakeInput(['1', '5', '7']))
    [1, 5, 7, 1, 5, 7, 1, 5, 7, 1]
    """
    return [int(input_func()) for _ in range(10)]

def print_4_inputs(input_func=input):
    """
    >>> print_4_inputs(FakeInput(['1', '5', '7']))
    1
    5
    7
    1
    """
    for i in range(4):
        print(input_func())

if __name__ == '__main__':
    doctest.testmod()

This also makes your functions more general so you can easily change them to take input from a file rather than the keyboard.

Caridorc
  • 6,222
  • 2
  • 31
  • 46
0

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
s.paszko
  • 633
  • 1
  • 7
  • 21