Saturday, April 9, 2016

Pytest: How to test a function with input call?

Leave a Comment

I have a console program written in Python. It asks the user questions using the command:

some_input = input('Answer the question:', ...) 

How would I test a function containing a call to input using pytest? I wouldn't want to force a tester to input text many many times only to finish one test run.

4 Answers

Answers 1

You should probably mock the built-in input function, you can use the teardown functionality provided by pytest to revert back to the original input function after each test.

import module  # The module which contains the call to input  class TestClass:      def test_function_1(self):         # Override the Python built-in input method          module.input = lambda: 'some_input'         # Call the function you would like to test (which uses input)         output = module.function()           assert output == 'expected_output'      def test_function_2(self):         module.input = lambda: 'some_other_input'         output = module.function()           assert output == 'another_expected_output'              def teardown_method(self, method):         # This method is being called after each test case, and it will revert input back to original function         module.input = input   

A more elegant solution would be to use the mock module together with a with statement. This way you don't need to use teardown and the patched method will only live within the with scope.

import mock import module  def test_function():     with mock.patch.object(__builtin__, 'input', lambda: 'some_input'):         assert module.function() == 'expected_output' 

Answers 2

As The Compiler suggested, pytest has a new monkeypatch fixture for this. A monkeypatch object can alter an attribute in a class or a value in a dictionary, and then restore its original value at the end of the test.

In this case, the built-in input function is a value of python's __builtins__ dictionary, so we can alter it like so:

def test_something_that_involves_user_input(monkeypatch):      # monkeypatch the "input" function, so that it returns "Mark".  This simulates the user entering "Mark" in the terminal:     monkeypatch.setitem(__builtins__, 'input', lambda: "Mark")      # go about using input() like you normally would:     i = input("What is your name?")     assert i == "Mark" 

Answers 3

You can do it with mock.patch as follows.

First, in your code, create a dummy function for the calls to input:

def __get_input(text):     return input(text) 

In your test functions:

import my_module from mock import patch  @patch('my_module.__get_input', return_value='y')) def test_what_happens_when_answering_yes(self, mock):     """     Test what happens when user input is 'y'     """     # whatever your test function does 

For example if you've a loop checking that the only valid answers are ['y', 'Y', 'n', 'N'] you can test that nothing happens when entering a different value instead.

In this case we assume a SystemExit is raised when answering 'N':

@patch('my_module.__get_input') def test_invalid_answer_remains_in_loop(self, mock):     """     Test nothing's broken when answer is not ['Y', 'y', 'N', 'n']     """     with self.assertRaises(SystemExit):         mock.side_effect = ['k', 'l', 'yeah', 'N']         # call to our function asking for input 

Answers 4

You can replace sys.stdin with some custom Text IO, like input from a file or an in-memory StringIO buffer:

import sys  class Test:     def test_function(self):         sys.stdin = open("preprogrammed_inputs.txt")         module.call_function()      def setup_method(self):         self.orig_stdin = sys.stdin      def teardown_method(self):         sys.stdin = self.orig_stdin 

this is more robust than only patching input(), as that won't be sufficient if the module uses any other methods of consuming text from stdin.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment