简体   繁体   中英

Python using mock for a multiple user inputs

A follow on for this question .

I am accepting user input in a for loop and have written a test case, test_apple_record . In this for loop, it queries a method self.dispatch_requested() ( not shown) which can randomly return True or False. Based on this answer, the code asks user for another input -- where the tray should be dispatched.

I am using the side_effect argument for mock.patch . How to automatically pass the hotel number as the user input using mock? I still want to continue passing the numbers [5, 6, 7] to the for loop, but now also want to pass in the hotel number based on response by self.dispatch_requested()

thank you

class SomeClass(unittest.TestCase):
    def apple_counter(self):
        apple_record = {}

        for i in range(3):
            apple_tray = input("enter tray number:")
            apple_record[apple_tray]  =  (i+1)*10
            print("i=%d, apple_record=%s"%(i, apple_record))

            if self.dispath_requested():
                number = input("Enter Hotel number to dispatch this tray:")
                update_hotel_record(number, apple_tray)

    def update_hotel_record(self, number, tray):
        self.hotel_record[number] = tray

    def test_apple_record(self):
        with mock.patch('builtins.input', side_effect=[5, 6, 7]):
            self.apple_counter()

Turns out my last answer was not useless after all! As there is no way of knowing which input you require but to read the prompt, you could simply replace the input() function with one that gives different answers depending on the prompt.

# first we need a generator for each type of response to `input()`

def tray_number_generator():
    trays = ["1", "5", "7"]
    for i in trays:
        yield i

trays = tray_number_generator()

def room_number_generator():
    rooms = ["112", "543", "724"]
    for i in rooms:
        yield i

rooms = room_number_generator()

# this can be written simpler as a generator expression like this:

trays = (tray for tray in ["1", "5", "7"])
rooms = (room for room in ["112", "543", "724"])

# now you can write a function that selects the next output depending on the prompt:

def mock_input(prompt):
    if "room" in prompt.lower():
        return next(rooms)
    if "tray" in prompt.lower():
        return next(trays)

# this can now be used to replace the `input()` function

with mock.patch('builtins.input', mock_input):
    do_stuff()

You actually want your side_effect to look like this:

m_input.side_effect = [1, 100, 2, 200, 3, 300]

Each time the input method is called, it will return the next item. So each time in your loop, you call input twice.

Also, I don't know the final structure of your unit test, however, seeing that you have a conditional statement around the second input that is called in your loop, you should probably set a mock around that method to always return True.

When you get to the scenario where you want to test your code for when self.dispath_requested() returns false, you have to keep in mind the second input will not be called, so your side_effect has to be re-written accordingly to match the expected behaviour for your code.

Also, finally, again, I'm not sure what your code actually looks like, however, based on how you seem to have your actual implementation and test code under the same class, I strongly advise not doing that. Try a structure similar to this:

Create a separate test class:

class Tests(unittest.TestCase):
    def setUp(self):
        self.s = SomeClass()

    @patch('__builtin__.input')
    def test_apple_record(self, m_input):
        m_input.side_effect = [1, 100, 2, 200, 3, 300]
        self.s.apple_counter()


if __name__ == '__main__':
    unittest.main()

So, you create an instance of SomeClass, and then this in effect will let you mock out the properties of the object much easier, which will make your unit tests much easier to write.

You will also notice that I used a decorator (@patch) instead of the "with" context. It's a personal preference, and I find it much easier to read the code using decorators.

Hope this helps.

I don't want go deeply in how mock both input and dispatch_requested and couple the answers to have a complete control and write a good unit test for this method. I think it is more interesting how to change your design to make the test (and so the code) simpler and more clear:

class SomeClass(object):
    def apple_counter(self):
        apple_record = {}

        for i in range(3):
            apple_tray = input("enter tray number:")
            apple_record[apple_tray]  =  (i+1)*10
            print("i=%d, apple_record=%s"%(i, apple_record))
            self._dispatch_and_ask_number()

    def _dispatch_and_ask_number(self):
        if self.dispatch_requested():
            number = self._ask_hotel_number()
            update_hotel_record(number, apple_tray)

    def _ask_try_number(self):
        return input("enter tray number:")

    def _ask_hotel_number(self):
        return input("Enter Hotel number to dispatch this tray:")

    def update_hotel_record(self, number, tray):
        self.hotel_record[number] = tray

Now you are in a better position to create a new class with just one responsibility of ask user input and then mock it to have a complete control in your test:

class AskUserInput(class):
    try_number_message = "Enter tray number:"
    hotel_number_message = "Enter Hotel number to dispatch this tray:"

    def try_number(self):
        return input(self.try_number_message)

    def hotel_number(self):
        return input(self.hotel_number_message)

And SomeClass can be changed like:

class SomeClass(object):

    _ask = AskUserInput()

    def apple_counter(self):
        apple_record = {}

        for i in range(3):
            apple_tray = self._ask.try_number()
            apple_record[apple_tray]  =  (i+1)*10
            print("i=%d, apple_record=%s"%(i, apple_record))
            self._dispatch_and_ask_number()

    def _dispatch_and_ask_number(self):
        if self.dispatch_requested():
            number = self._ask.hotel_number()
            update_hotel_record(number, apple_tray)

    def update_hotel_record(self, number, tray):
        self.hotel_record[number] = tray

And finally the test

class TestSomeClass(unittest.TestCase):
    @patch("AskUserInput.try_number")
    @patch("AskUserInput.hotel_number")
    def test_apple_record(self, mock_try_number, mock_hotel_number):
        # Now you can use both side_effects and return_value
        # to make your test clear and simple on what you test.

If you are playing with legacy code this approch is not really useful, but if you are testing something that you are developing now is better to turn it in a more testable code: make your code more testable improve design almost every times.

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