简体   繁体   中英

Python3: Testing repeated user input

I'm a bit of a beginner in terms of unit-testing and still trying to get familiar with some of the things. This is part of my own project and I'm stuck on how to test it fully.

There was a question in this direction already, but it did only concern itself on how to repeatedly ask the user for input, not on how to unittest it.

Goal:

I have a function that asks the user for input and repeats the request if the input is invalid. My goal is to figure out how to test if the input-request is repeated if the user gives invalid input. By that I mean, I'm trying to test whether the mechanism to repeat the input-request under the defined circumstances works as intended.

The Code:

The function asks for a regular expression from the user and compiles it into an SRE_Pattern object from the re-package (python standard library). If the user provides an input and it's not a valid expression, the input-request is repeated.

import re

def request_regex_pattern(input_message):
    regex_pattern = None
    while True:
        regex = input(input_message)

        if not regex:
            print("No Regex provided.")
            break

        try:
            regex_pattern = re.compile(regex, re.IGNORECASE)
            break
        except re.error:
            print("The input was not valid regular expression")
            continue

    return regex_pattern

Tests so far:

What I can test so far is whether for valid input (eg \d\d ) I get correct output ( SRE_Pattern object of that regular expression) using mocking.

import unittest as ut
from unittest import mock
import re

class TestUserInput(ut.TestCase):
    def test_request_regex_pattern(self):
        with mock.patch('builtins.input', return_value='\d\d'):
            test_pattern = request_regex_pattern('')
            test_string = '01'
            self.assertIsNotNone(test_pattern.match(test_string))

I've thought about this and googled for a while now but couldn't come to a satisfying answer.

Is there a sensible way to test whether the input-request was repeated? What are the best-practices there?

Using python's default unittest library is not mandatory for solutions. However, generally solutions using the standard libraries would be preferred as that reduces the number of requirements needed for the project I'm working on.

Thank you very much for your time !

MrBreanBremen pointed me in the right direction to linking me to his answer and kindly helped me with some of my follow-up questions regarding syntax and understanding the principle. I want to elaborate and dumb down the principles behind what he used there a bit for easier understanding.

The Principle

The way to check this is by patching a function that is called under the circumstances that you want to check. This replaces them with MagicMock-objects that know when they have been called and with which parameters !

Once the function is patched, you can then use that MagicMocks assert methods such as assert_called_with() , assert_called_once() , assert_called() and/or assert_not_called() to check whether the function that this MagicMock object replaced was called at all and with the parameters you were expecting.

Now how does this apply to this question?

First back to what is our problem. We have 3 Test-cases:

1) User provides valid regular expression that can compile into an SRE_Pattern object --> Returns an SRE_Pattern object

2) User provides no input (just hits enter) --> Returns None

3) User provide input that can't be compiled into an SRE_Pattern object (invalid input), triggering the print("The input was not valid regular expression") statement --> Never Returns anything

We only care about circumstances of 3), as case 1) and 2) have a defined output that we can check easily with "normal" unit-tests, while case 3) explicitly can't output anything, leading us to our problem.

As already hinted at in 3), under these circumstances only the print function is called. Meaning that for our test we should patch it, so that we can receive a MagicMock-object for this function and use its assert_called_with() together with the string that print-statement gets, as that string only occurs in that section of code. It is "unique" for these circumstances !

We have one more issue to solve though. Once we patch the builtins.input as before, but with something that causes our while-loop to repeat, we will still be stuck in a while-loop! The function call request_regex_pattern() will never end, Since we can't exit the function normally, we'll have to exit by causing an Exception . Thus, we need to also patch that into one of the functions that is called when these circumstances happen. In this case, we can conveniently add this side-effect to our patch of print . We can then catch that Exception with a context-manager with self.assertRaises(Exception) to keep our test from failing.

The code:

import unittest as ut
from unittest import mock
import re

class TestUserInput(ut.TestCase):
    def test_request_regex_pattern_non_regex_input(self):
        with mock.patch('builtins.input', return_value='\l\d'):
            with mock.patch('builtins.print', side_effect=[None, Exception('To Break the Loop!')]) as mocked_print:
                with self.assertRaises(Exception):
                    ui.request_regex_pattern('')
                mocked_print.assert_called_with('The input was not valid regular expression')

Improving Readability

This was kept with the "patch through context-manager"-syntax for consistency with the previously displayed unit-test.

As you can see, that code is not nice to read due to all the context-managers. As such, it is better to use the patch-decorator instead, as MrBreanBremen did, This will then pass the MagicMock-objects for these functions as parameters to your test. in the order that the patches are applied. Here mocked_input is the MagicMock object of the patched input() method and mocked_print is the MagicMock object of the patched print() method.

import unittest as ut
from unittest import mock
import re

class TestUserInput(ut.TestCase):
    @mock.patch('builtins.input', return_value='\l\d')
    @mock.patch('builtins.print', side_effect=[None, Exception('To Break the Loop!')])
    def test_request_regex_pattern_non_regex_input(self, mocked_input, mocked_print):
        with self.assertRaises(Exception):
            request_regex_pattern('')
        mocked_print.assert_called_with('The input was not valid regular expression')

A reliable way to prevent the user from entering the same input more than once would be to simply use a python built-in list. You can use a pre-determined size and just store that many elements inside it. You can append elements to the list and then if the predetermined size is exceeded pop elements from the front (oldest elements). This way you would be able to make sure that the user wouldn't be able to enter the same input as the last N (size of the list) inputs.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM