1

I'm learning how to use Python. I have a function with a conditional inside of it, if an invalid input is provided, it should restart the loop until a valid input is provided.

Unfortunately, this "restarting" behavior is causing an infinite loop within my tests (it circularly provides the wrong input). How can I pause, or break, or limit the output to one instance so I can test the returned string?

function:

def confirm_user_choice(choice: str):
    while True:
        user_response = input(f"\nYou chose '{choice}', is this correct? y/n ")
        if user_response == "y":
            return True
        elif user_response == "n":
            return False
        else:
            print("\nSelect either 'y' (yes) or 'n' (no)")

test:

import unittest
from unittest import mock
from src.utils.utils import addValues, confirm_user_choice


class TestConfirmUserChoice(unittest.TestCase):
    def test_yes(self):
        with mock.patch("builtins.input", return_value="y"):
            result = confirm_user_choice("y")
        self.assertEqual(result, True)

    def test_no(self):
        with mock.patch("builtins.input", return_value="n"):
            result = confirm_user_choice("n")
        self.assertEqual(result, False)

    def test_invalid_input(self):
        with mock.patch("builtins.input", return_value="apple"):   <-- triggers func else case
            result = confirm_user_choice("apple")
        self.assertEqual(result, False)

3 Answers 3

1

You have a partial function: on a proper input, it will return a Boolean value, but it may not return at all, and you can't test that an infinite loop is indeed infinite.

To make it more testable, allow the function to take an optional iterable value that defaults to sys.stdin, allowing you to control what the function reads (and how long it will attempt to do so.)

def confirm_user_choice(choice: str, responses: Optional[Iterable[str]] = None):
    if responses is None:
        # An infinite stream of calls to input()
        responses = iter(lambda: input(f"\nYou chose '{choice}', is this correct? y/n "), None)

    for user_response in responses:
        if user_response == "y":
            return True
        elif user_response == "n":
            return False
        else:
            print("\nSelect either 'y' (yes) or 'n' (no)")
    else:
        # Note: cannot be raised from the default value of responses
        raise ValueError("Unexpected end of responses")

Now your test can simply pass canned lists of responses, and either catch the expected ValueError, or look at the returned Boolean value.

import unittest
from src.utils.utils import addValues, confirm_user_choice


class TestConfirmUserChoice(unittest.TestCase):
    def test_yes(self):
        result = confirm_user_choice("y", ["y"])
        self.assertTrue(result)

    def test_eventual_yes(self):
        result = confirm_user_choice("y", ["apple", "pear", "y"])
        self.assertTrue(result)

    def test_no(self):
        result = confirm_user_choice("y", ["n"])
        self.assertFalse(result)

    def test_no_valid_input(self):
        with self.assertRaises(ValueError):
            result = confirm_user_choice(["apple"])
Sign up to request clarification or add additional context in comments.

1 Comment

That is a very informative answer. Thank you so much.
1

continue does nothing in your code
continue alows you to ignore a part of the code for some instance of the loop.

For example :

for i in range(2):
   if i < 1:
      continue
   print(i)

Output :

1

For what you want to do, don't forget while is suppose to end when a condition is meet. Hence bypassing the condition using while True: and then using a if to exit your loop is a bit counter productive.

Just use the while condition :

user_response = ""
while user_response not in ["y", "n"]:
    user_response = input("y/n ? ")
print(user_response)

Happy programming

Comments

0

I'm new to Python myself but in my understanding, unit tests investigate how function handle different inputs based on the function's return value or exceptions raised (if any).

Your function only exits when the user inputs either "y" or "n" or when an error is raised (for instance, if the user provides Crtl-Z). Your while loop does not break when a user inputs 'apple.' There is no return value for pytest (or the like) to inspect.

If you really want to test this, you'd have to rewrite your function so that's a little more modular. It would have to feature at least three different return values, including one that implies that the input was invalid.

2 Comments

I think there are tests that allow me to confirm returned print statements. Problem is, even if I use that, I can't get around the looped behavior. Running that test fills my console with the else response and ... as of yet i haven't determined how to just simply test its occurrence (once). I'll explore other ways to write it, at the moment though I feel like there is no way around looping the func until either 'y' or 'n' is provided 🤔
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.