Testing Python functions that require user input()

Viewed 71

I want to test the following function:

def get_inputs():
    str_from_user = input()
    list_of_marks = []
    while str_from_user != "exit":
        list_of_marks.append('!')
        str_from_user = input()
    return list_of_marks

is there a way to test multiple scenarios? something like this (but only that works):

def test_get_inputs():
    assert get_inputs(){"hey", "coco", "milo", "exit"} == "!!!"
    assert get_inputs(){"exit"} == ""
    assert get_inputs(){"horses have long faces", "exit} == "!"
1 Answers

There is no obvious way to test your function as it is currently written, because you are reading from standard input. This is a side effect that makes your function non-pure and hard to test. In other words, the input comes from outside your program, so you do not have control over it.

You should rewrite your function in a way that makes it easy to provide input and check output within the program. You can do this passing a function to use as a way to get input instead of always using input(). Then, for testing, you can create a simple iterator object over a list of known inputs and pass its __next__ method as input function.

Here's an example of how this could be done:

def get_inputs(input_function):
    str_from_user = input_function()
    list_of_marks = []
    while str_from_user != "exit":
        list_of_marks.append('!')
        str_from_user = input_function()
    return list_of_marks

def test_get_inputs():
    input_func_one   = iter(["hey", "coco", "milo", "exit"]).__next__
    input_func_two   = iter(["exit"]).__next__
    input_func_three = iter(["horses have long faces", "exit"]).__next__
    assert get_inputs(input_func_one) == ['!', '!', '!']
    assert get_inputs(input_func_two) == []
    assert get_inputs(input_func_three) == ['!']

Another option, as suggested by @chepner in the above comments, would be to pass the function a file-like object, which can be easily mocked using the io module with either BytesIO or StringIO. Take care in this case though because input() strips the trailing newline, but StringIO.readline() does not.

from io import StringIO

def get_inputs(input_file):
    str_from_user = input_file.readline().rstrip()
    list_of_marks = []
    while str_from_user != "exit":
        list_of_marks.append('!')
        str_from_user = input_file.readline().rstrip()
    return list_of_marks

def test_get_inputs():
    assert get_inputs(StringIO("hey\ncoco\nmilo\nexit\n")) == ['!', '!', '!']
    assert get_inputs(StringIO("exit\n")) == []
    assert get_inputs(StringIO("horses have long faces\nexit\n")) == ['!']

You could also adapt the first case (passing an input function) to work with StringIO by simply passing StringIO("...").readline as function.


Now that you have a function that is easily testable, you can write a simple wrapper that uses the function and works with standard input instead:

# If using an input function
def get_inputs_from_stdin():
    return get_inputs(input)

# If using a file-like object
import sys

def get_inputs_from_stdin():
    return get_inputs(sys.stdin)
Related