2

I want to test with unittest a function in Python (2.7) that use different raw_input.

How can I achieve this?

Function (in module fc):

def main():
    name            = raw_input("name: ").lower()
    surname         = raw_input("surname: ").lower()
    birth_date    = raw_input("Birth date (dd/mm/yyyy): ").lower()
    city          = raw_input("city: ").lower()
    sex           = raw_input("sex (m/f): ").lower()
    #other tasks...

test function:

import fc
import unittest

class test_main_fc(unittest.TestCase):

    def test_main(self):
        #how can I give to main the parameters that will ask?
        self.assertEqual(fc.main(), 'rssmra80a01l781k')             

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

The solution I could find, this, works for 1 input passed at a time. I want to know how to pass different values to the main function.

This works for only 1 value of raw_input requested, in this case, name.

class test_main_fc(unittest.TestCase):

    def test_fc_output(self):

        original_raw_input = __builtins__.raw_input
        __builtins__.raw_input = lambda _: 'mario'

        #capturing the output
        with captured_output() as (out, err):
            fc.main()
            output = out.getvalue().strip()

        self.assertEqual(output, 'rssmra80a01l781k')

        __builtins__.raw_input = original_raw_input
Pleasant94
  • 471
  • 2
  • 8
  • 21
  • In general it is better not to test the function that calls `raw_input`, but the function that uses its results. So either there is a function that processes *values* (regardless of where they were obtained from), or you inject into `main` a function to use for getting input. The former case seems cleaner in general. The latter case allows you to stub the function in tests so it returns what you want. – RecencyEffect Jan 14 '18 at 18:38
  • In my case, I should test the code without changing it, so I was looking for a way. – Pleasant94 Jan 14 '18 at 18:45
  • If this is an absolute requirement (I would argue with colleagues whether that were the case), then use Ulrich Eckhardt's suggestion, where his `fake_input` is assigned to `__builtins__.raw_input` (as shown in the link you posted). – RecencyEffect Jan 14 '18 at 18:49

1 Answers1

1

In order to achieve that, you need to replace raw_input with a function that returns different things when called multiple times, e.g. one like this:

answers = [1, 2, 4]
fake_input = answers.pop
# 4
print(fake_input())
# 2
print(fake_input())
# 1
print(fake_input())
# raise IndexError
print(fake_input())

You install this similar to the existing function:

# replace raw_input
original_raw_input = __builtins__.raw_input
__builtins__.raw_input = ['input', 'fake', 'my'].pop
# ... code that uses raw_input
print raw_input()
print raw_input()
print raw_input()
# restore raw_input
__builtins__.raw_input = original_raw_input

Some notes:

  • I don't know why the original author used __builtins__.raw_input instead of just assigning to raw_input (which requires declaring it as global raw_input when in a function, of course). I haven't tried if it makes any difference.
  • In your test, if the output is not as expected, the original raw_input is not restored! While this may be acceptable in a test, it is still a bug. The easy way out is to use try ... finally, restoring raw_input in the finally code. There's a better way though: Use a so-called context manager. captured_out is one example, it redirects the output for some code and guarantees that the original output target is restored. Take this as exercise, it's very useful to understand this technique.
  • The order is reversed, since pop removes elements from the back. You could first feed the sequence to reversed, but that would make the code even less readable and understandable. Wrapping it into a context manager would hide these details.
  • Don't forget to upgrade to Python 3!
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
  • It gives me an error when in the code is called the `lower()` function: AttributeError: 'builtin_function_or_method' object has no attribute 'lower' – Pleasant94 Jan 14 '18 at 18:52
  • Wait: Do you understand how the code you have works for one input? – Ulrich Eckhardt Jan 14 '18 at 19:03
  • I'm new to Python, so not very well. I assume that `original_raw_input` redefines the builtin function `raw_input`, so that when it's called, I could use `original_raw_input`? – Pleasant94 Jan 14 '18 at 19:27
  • In `__builtins__.raw_input` is the function that is called when some code calls `raw_input`. The hack you use changes this function to a different function to simulate input. It used a lambda function `lambda _:'mario'` which always returned the same value. My code here was just intended to show you how to write a function that returns different values by using a list's `pop` function. You can't take this code as it stands and plug it into your code though, you need to take some time to understand this and to adapt it, like returning the expected strings instead of some random integers. – Ulrich Eckhardt Jan 15 '18 at 07:31
  • Ok solved. Re-assuming: I save the `__builtins__.raw_input` function in `original_raw_input`, then I ovveride it with the lambda function: `__builtins__.raw_input = lambda _: fake_input()`. The function `fake_input()` uses the `pop` function on a list of values. Then I reset the `__builtins__.raw_input`. Python is really fascinating. – Pleasant94 Jan 15 '18 at 09:11
  • Almost! :) your `lambda` creates a function which just calls another function with the exact same arguments (no arguments, to be precise). Just use that function directly. I have added an according example to the answer. – Ulrich Eckhardt Jan 16 '18 at 04:43