2

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} == "!"
4
  • 5
    The actual goal should be to avoid writing functions with such side-effects. Write pure functions, that can be tested trivially. Commented Jul 19, 2022 at 14:08
  • 1
    mock input function. Commented Jul 19, 2022 at 14:09
  • You can use mocking but I have only worked with unittest so not sure how it works with pytest. Though you should rethink the design of the tests. Commented Jul 19, 2022 at 14:09
  • One idea: give get_inputs a file-like object parameter f that defaults to sys.stdin, then use f.readline() instead of input to read the input. Then you can test your function with a io.StringIO argument instead of mocking anything. Your production code can continue to call the function with no explicit argument to read from standard input. Commented Jul 19, 2022 at 14:14

1 Answer 1

2

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)
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.