Unittesting for correct 'continue' behaviour - python-3.x

I have a function that asks a user for confirmation via a prompt. It accepts y or n as answers, otherwise it asks again.
Now, I want to write a unittest for this function. I can test the correct behaviour for y or n just fine, but how do I test that my function correctly rejects inacceptable input?
Here's the code for foo.py:
def get_input(text):
"""gets console input and returns it; needed for mocking during unittest
"""
return input(text)
def confirm(message='Confirm?', default=False):
"""prompts for yes or no response from the user. Returns True for yes and
False for no.
'default' should be set to the default value assumed by the caller when
user simply types ENTER, and is marked in the prompt with square brackets.
"""
if default:
message = '%s [y]|n: ' % (message) # default answer = yes
else:
message = '%s y|[n]: ' % (message) # default answer = no
while True:
answer = get_input(message).lower()
if not answer:
return default
if answer not in ['y', 'n']:
print('Please enter y or n!')
continue
if answer == "y":
return True
if answer == 'n':
return False
answer = confirm()
print(answer)
And here is my Test class:
import unittest
import foo
class TestFoo_confirm(unittest.TestCase):
"""testing confirm function
"""
#unittest.mock.patch('foo.get_input', return_value='y')
def test_answer_yes(self, _):
self.assertEqual(foo.confirm(), True) # confirmed if 'y' was entered
So, how do I write a similar test for an input-value like '1' (or how do I need to adjust my confirm() function to make it testeable)?
Currently, if I call foo.confirm() from the unittest file, it just gets stuck in an infinite loop and it doesn't return anything. (I understand why this is happening, just not how to circumvent it.)
Any ideas?

You could try this:
import unittest, unittest.mock
import foo
class TestFoo_confirm(unittest.TestCase):
"""testing confirm function
"""
#unittest.mock.patch('foo.get_input', return_value='y')
def test_answer_yes(self, _):
self.assertEqual(foo.confirm(), True) # confirmed if 'y' was entered
#unittest.mock.patch('builtins.print')
#unittest.mock.patch('foo.get_input', side_effect=['1','yn','yes','y']) # this will make the mock return '1', 'yn' and so on in sequence
def test_invalid_answer(self, mock_input, mock_print):
self.assertEqual(foo.confirm(), True) # it should eventually return True
self.assertEqual(mock_input.call_count, 4) # input should be called four times
mock_print.assert_called_with('Please enter y or n!')
In the second test case, we imitate a user who enters three invalid inputs, and, after being prompted again, finally enters 'y'. So we patch foo.get_input in such a way that it returns 1 the first time it's called, then yn, then yes and finally y. The first three examples should cause the confirm function to prompt the user again. I also patched the print function, so that the 'Please enter y or n!' message wouldn't show up when testing. This isn't necessary.
Then we assert that our mock input was called four times, meaning that the first three times, the confirm function reprompted.
Finally we assert that the print function was called (at least once) with 'Please enter y or n!'.
This does not test if the correct number of print statements were made or if they were in correct order, but I suspect this would be possible too

Related

regex doesn't seem to work in allowRegexes parameter when using pyinputplus

I am using pyinputplus and specifically inputNum
https://pyinputplus.readthedocs.io/en/latest/
This is what my code looks like:
msg = 'Enter value to add/replace or s to skip field or q to quit: '
answer = pyip.inputNum(prompt=msg, allowRegexes=r'^[qQsS]$', blank=False)
My goal is to allow any number but also allow one of the following q,Q,s,S.
However when I run the code and enter 'sam' the code crashes because later on I am trying to convert to float(answer).
My expectation is that allowRegexes will not allow this and will show me the prompt again to re-enter.
Please advise!
It seems pyip.inputNum stops validating the input if you provide allowRegexes argument. See the allowRegexes doesn't seem work properly Github issue.
You can use the inputCustom method with a custom validation method:
import pyinputplus as pyip
import ast
def is_numeric(x):
try:
number = ast.literal_eval(x)
except:
return False
return isinstance(number, float) or isinstance(number, int)
def raiseIfInvalid(text):
if not is_numeric(text) and not text.casefold() in ['s', 'q']:
raise Exception('Input must be numeric or "q"/"s".')
msg = 'Enter value to add/replace or s to skip field or q to quit: '
answer = pyip.inputCustom(raiseIfInvalid, prompt=msg)
So, if the text is not and int or float and not equal to s/S/q/Q, the prompt will repeat showing up.

How can I test a loop with multiple input calls?

I'm trying to test a fuction that dependets a of multiple user inputs to return some value.
I've already looked for multiples unswers here but none was able to resolve my problem. I saw things with parametrize, mock and monkey patch but none helped. I think a lot is because I don't clearly understood the concepts behind what was being done and I couldn't adapt to my problem. I saw suggestion of using external file for this but I don't wont to depend on that. I'm trying with pytest and python 3.7.3
The function that I want to test is something like this
def function():
usr_input = input('please enter a number: ')
while True:
if int(usr_input) < 5:
usr_input = input('please, enter a value less then 5: ')
else:
break
return usr_input
I want to know how can I pass two input values to test the function when the inserted value is not valid. Example: Send value 6 and 2, make an assert expecting value 2 and pass the test. My others tests look like this:
def test_input(monkeypatch):
monkeypatch.setattr('builtins.input', lambda x: 6)
test = function()
assert test == 2
but, for this case, they loop. It's possible to do this only with parametrize or other simple code?
EDIT
I added a int() in my "if", as wim pointed in the accepted answer, just to prevent any confusion for future readers. I'm assuming the cast is possible.
Two problems here, you need to convert the input into a number otherwise the comparison will fail, comparing a string with a number: usr_input < 5. Note that the real input will never return a number, only a string.
Once you've cleared that up, you can monkeypatch input with a callable that can return different values when called:
def fake_input(the_prompt):
prompt_to_return_val = {
'please enter a number: ': '6',
'please, enter a value less then 5: ': '2',
}
val = prompt_to_return_val[the_prompt]
return val
def test_input(monkeypatch):
monkeypatch.setattr('builtins.input', fake_input)
test = function()
assert test == 2
If you install the plugin pytest-mock, you can do this more easily with the mock API:
def test_input(mocker):
mocker.patch('builtins.input', side_effect=["6", "2"])
test = function()
assert test == 2

It asks to give the values but instead of giving an answer. It is giving me None

Here I created a module.
class Employee:
def __init__(self):
self.name = input("Enter your name: ")
self.account_number = int(input("Enter your account number: "))
def withdraw(self): # it receives values from for
if withdraw1 > current_balance:
print ("You have entered a wrong number: ")
else:
print ("The current balance is: ", current_balance - withdraw1)
import TASK2 # I am importing the module I created
c = TASK2.Employee()
def for(self):
c.withdraw1 = int(input("enter number: "))
c.current_balance = int(input("Enter the current balance: "))
d = method(c.withdraw) # here I am trying to pass the values to withdraw
print (d)
The problem I get is that although it asks for the values instead of giving me an answer it gives me None.
Here's my take on your code.
# TASK2.py
class Employee:
def __init__(self):
self.name = input("Enter your name: ")
self.account_number = int(input("Enter your account number: "))
# make sure you initialise your member variables!
self.withdraw_val = 0 # withdraw1 is ambiguous, so I use withdraw_val instead
self.current_balance = 0
# receives values from for ### no it doesn't, right now, it GIVES values TO your "for" function
def withdraw(self):
if self.withdraw_val > self.current_balance: # remember to use "self." to
# access members within the class
print ("You have entered a wrong number: ")
else:
# again, remember "self."
print ("The current balance is: ", self.current_balance - self.withdraw_val)
# TASK2sub.py
import TASK2
c = TASK2.Employee()
def for_employee(employee): # (1) don't use "self" outside a class
# it's contextually unconventional
# (2) "for" is a keyword in Python, don't use it for naming
# variables/functions, it'll mess things up
employee.withdraw_val = int(input("Enter value to withdraw: "))
employee.current_balance = int(input("Enter the current balance: "))
return employee.withdraw_val # not entirely sure what you want to return
# but you should definitely return something
# if you're going to assign it to some variable
d = for_employee(c.withdraw()) # "for_employee" function needs a return statement
# ".withdraw()" method should also require a return statement
print(d)
Note: I'll be referring to your original for function as for_employee from now on. Also note that I'm still hazy about what you're trying to accomplish and that there is most probably a more suitable name for it.
Since your original for_employee function didn't return anything, it returns None by default. (This explains the output you saw.)
I think you're misunderstanding how functions work in general. For example,
d = for_employee(c.withdraw())
print(d)
Your comment for the .withdraw() method is inaccurate.
"it receives values from for"
More accurately, c.withdraw() will first be computed, then whatever it returns is passed into the for_employee function as a parameter. Instead of "receiving values from", the withdraw method "gives values to" the for_employee function.
Something more reasonable would be
c.withdraw() # on a line by itself, since it doesn't return anything
d = for_employee(c) # pass the entire object, since you'll be using self.withdraw_val and whatnot
print(d)
Another issue is with conventional naming. This is what I get from the IDLE (with Python 3.7) when defining a function named for
>>> def for(a): return a
SyntaxError: invalid syntax
Again, for is a keyword in Python, don't use it for naming your variables, functions, or classes.
With self, it's less severe (but I could see that it's confusing you). self is more of a convention used in class methods. But for_employee isn't a class method. So conventionally speaking, the parameter shouldn't be named self.
(I find the code spaghetti-ish, it might benefit if you refactor the code by moving the for_employee method into the class itself. Then it would completely make sense to use self.)

Why are we still stuck on this function despite threading?

I want to make a function that waits for input, and if nothing is input in 2 seconds, skips the input and moves on to the rest of the function.
I tried this function from another thread:
import time
from threading import Thread
answer = None
def check():
time.sleep(2)
if answer != None:
return "ayy"
print("Too slow")
return "No input"
Thread(target = check).start()
answer = input("Input something: ")
print(answer)
This code asks for input, and if no input is added in 2 seconds it prints "too slow". However it never moves on to print(answer), I think it keeps waiting for user input.
I want to ask for user input and if it takes too long, it just takes input = None and moves on to the functions underneath it. I looked at timeout methods involving signal, but that's only for linux and im on a windows.
Your assumption is right. The input() call is waiting for the user to submit any input which in your case never happens.
A cross platform solution would be to make use of select():
import sys
import select
def timed_input(prompt, timeout=10):
"""
Wait ``timeout`` seconds for user input
Returns a tuple:
[0] -> Flag if timeout occured
[1] -> User input
"""
sys.stdout.write(prompt)
sys.stdout.flush()
input, output, error = select.select([sys.stdin], [], [], 2)
if input:
return True, sys.stdin.readline().strip()
else:
return False, None
timed_input('Input something: ', timeout=2)
That's a dirty prototype. I suggest to use exceptions for the timeout or a more intuitive return value for the function.

defining a function in python 3

the moment you guess the correct number, it freezes and doesn't proceed further, however if I don't define the function do_guess_round and instead simply write the code after the second while True statement it works perfectly. I guess I am incorrectly defining the function
import random
computers_number = random.randint(1,100)
prompt=('what is your guess? ')
def do_guess_round():
"""choose a random number, prompt the user to guess it
check whether the user is true,
and repeat the pocess untill the user is correct"""
while True:
players_guess=input (prompt)
if computers_number == int(players_guess):
print ('correct! well done')
break
elif computers_number<int(players_guess):
print ("incorrect, your guess is higher")
else:
print ('incorrect, your guess is lower')
print ("Starting a new round ")
print ("let the guessing game begin")
while True:
do_guess_round()
Current your do_guess_round() function does not do anything. You need to indent the code that is supposed to be contained in it. The while True beneath it is not indented, so the body of the function currently only consists of the string:
"""choose a random number, prompt the user to guess it
check whether the user is true,
and repeat the pocess untill the user is correct"""
At the moment you guess the correct number, your code collapses to
def do_guess_round():
while True:
break
while True:
do_guess_round()
No surprise it freezes, you haven't built any way for it to quit - break only leaves one level of loop. Presumably you're going to move the code to generate numbers into the function? You need something more like
def do_guess_round():
# read player guess here
if guess matches:
return True
else:
return False
result = False
while result != True:
result = do_guess_round()
def do_guess_round():
"""choose a random number, prompt the user to guess it
check whether the user is true,
and repeat the pocess untill the user is correct"""
while True:
The "while True" loop isn't indented, so it isn't in the function, indent the loop an it's contents, then you should be fine.

Resources