(Note: the original function was broken due to indentation so while I was fixing it I took the liberty of rewriting it to be simpler. It should still behave exactly the same as your intended original implementation and the testing strategy, which is the real focus of the question, is exactly the same regardless of implementation details.)
One way to do this is with patch
:
def valid_letter() -> str:
'''
Prompts the user for a column between 'a' and 'g'.
Continuously asks for a valid letter if invalid data is provided.
'''
while True:
column = input("What column do you wish to select from a to g? ")
if ord(column) in range(ord('a'), ord('g') + 1):
return column
print("Your input is invalid")
from unittest.mock import Mock, patch
def test_valid_letter() -> None:
with patch('builtins.input', new=Mock(return_value='a')):
assert valid_letter() == 'a'
with patch('builtins.input', new=Mock(side_effect=['z', 'q', 'g', 'c'])):
assert valid_letter() == 'g'
test_valid_letter()
The patch
statements in the test replace the builtin input
function with a Mock
object that returns a particular argument. In the first test it simply returns 'a'
, and so we assert that valid_letter()
will return that same value. In the second test it returns successive values from the list each time it's called; we assert that valid_letter()
will continue calling it in a loop until it reaches 'g'
.
Another method would be via dependency injection:
from typing import Callable
from unittest.mock import Mock
def valid_letter(input_func: Callable[[str], str]) -> str:
'''
Prompts the user for a column between 'a' and 'g', using input_func.
Continuously asks for a valid letter if invalid data is provided.
'''
while True:
column = input_func("What column do you wish to select from a to g? ")
if ord(column) in range(ord('a'), ord('g') + 1):
return column
print("Your input is invalid")
def test_valid_letter() -> None:
assert valid_letter(Mock(return_value='a')) == 'a'
assert valid_letter(Mock(side_effect=['z', 'q', 'g', 'c'])) == 'g'
test_valid_letter()
In this example, rather than having valid_letter
call the builtin input
function, it accepts an arbitrary input function that the caller supplies. If you call it like:
valid_letter(input)
then it behaves exactly like the original, but the caller can also pass in arbitrary replacements without having to use patch
. That makes testing a bit easier, and it also allows for the possibility of a caller wrapping or replacing input
to allow for a different UI style -- for example, if this function were being used in a GUI app the caller could pass in an input_func
that prompts the user via a dialog box instead of the terminal.
The same testing/injection logic applies to the print
function -- you might find it useful to have your test validate that print
is called each time an invalid value is entered, and another caller might find it useful to use an alternative output function.