Home > Blockchain >  python testing, how handle 'continue' statement loop
python testing, how handle 'continue' statement loop

Time:09-28

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)

CodePudding user response:

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"])

CodePudding user response:

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

CodePudding user response:

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.

  • Related