3

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()
Community
  • 1
  • 1
stackjs
  • 443
  • 2
  • 6
  • 13

3 Answers3

5

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.

idjaw
  • 25,487
  • 7
  • 64
  • 83
  • thanks @idjaw, but the problem is it depends on what `self.dispatch_requested()`. Sometimes it may not even call the second user input. thus, I can not use the side effect trick (with alternate numbers) in this case. – stackjs Sep 20 '15 at 18:20
  • 1
    You should mock the dispatch_requested as well to test for each of the True False scenarios. That way you set up your expected data and validate that the flow inside the method holds true for the different cases. Ultimately, any method called inside what you are unit testing needs to be mocked and controlled accordingly to test all the different cases that should come up. – idjaw Sep 20 '15 at 18:27
3

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()
Community
  • 1
  • 1
Azsgy
  • 3,139
  • 2
  • 29
  • 40
  • 1
    Just tried the above, it gives error about 'prompt' .. how do I pass the command prompt text to `mock_input`? – stackjs Sep 20 '15 at 18:39
  • @stackjs derp, you don't actually need the new_callable argument. Just pass the `mock_input` function as second parameter. I have updated my answer – Azsgy Sep 20 '15 at 19:08
  • There isn't any pros to don't use `side_effects` in your patch instead of replace function by a new one. From the other side you loose the ability to test how the code call `mock_input` method. – Michele d'Amico Sep 21 '15 at 12:02
2

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.

Michele d'Amico
  • 22,111
  • 8
  • 69
  • 76
  • thanks for an excellent answer! can not agree more on improving the design... very well explained! I am dealing with some legacy code, but there are some things I can always implement as you have suggested. I will keep the original accepted answer unchanged for now... but thanks again for this suggestion. – stackjs Sep 22 '15 at 18:08