Pytest: mocking / monkey patching builtin input() and print() functions in python

Question:

How can I monkey patch the builtin input and print functions using Pytest so as to capture the output of someone else’s code and test it with pytest before refactoring?

For example, I have acquired some code similar to this:

class QueryProcessor:
    def __init__(self ...):
        ...  

    def write_search_result(self, was_found):
        print('yes' if was_found else 'no')

    def read_query(self):
        return Query(input().split())

I don’t want to read dozens of input parameters from stdin, and I don’t want to print the outputs. I want to use the functions I’ve written that sift through a directory full of mytest.in and mytest.out files and pass the inputs to pytest using @pytest.mark.parametrize(...).

But I can’t figure out how to patch the awkward read… and write… functions in this class.

I suspect it’s something along the lines of:

@yptest.mark.parametrize("inputs…, expected outputs…", data_reading_func())
def test_QueryProcessor(monkeypatch, inputs…, expected outputs…):
   """Docstring
   """
   q = QueryProcessor()

   def my_replacement_read():
       ...
       return [...]

   def my_replacement_write():
       ...
       return [...]

   monkeypatch.???
   assert ...

Can you help?

Many thanks

Asked By: curlew77

||

Answers:

While awaiting a response, I came up with the following myself. I think the ideal answer will be what I’ve done implemented in the way @hoefling suggests—using patch.

@pytest.mark.parametrize("m, n, commands, expec", helpers.get_external_inputs_outputs('spampath', helpers.read_spam_input_output))
def test_QueryProcessor(monkeypatch, m, n, commands, expec):

    def mock_process_queries(cls):
        for cmd in commands:
            cls.process_query(Query(cmd.split())) # also mocks read_query()

    def mock_write_search_result(cls, was_found):
        outputs.append('yes' if was_found else 'no')

    monkeypatch.setattr('test.QueryProcessor.process_queries', mock_process_queries)
    monkeypatch.setattr('test.QueryProcessor.write_search_result', mock_write_search_result)

    outputs = []

    proc = QueryProcessor(m)
    proc.process_queries()

    assert outputs == expec

UPDATE:

@pytest.mark.parametrize("m, n, commands, expec",
                         helpers.get_external_inputs_outputs(
                             'spampath',
                             helpers.read_input_output))
def test_QueryProcessor_mockpatch(m, n, commands, expec):

    commands.insert(0,n)

    mock_stdout = io.StringIO()

    with patch('spammodule.input', side_effect=commands):
        with patch('sys.stdout', mock_stdout):
            proc = hash_chains.QueryProcessor(m)
            proc.process_queries()

    assert mock_stdout.getvalue().split('n')[:-1] == expec
Answered By: curlew77

Hey there I guess it is too late for you but for the other people asking themselves how to monkeypatch input(), I did it like this:

monkeypatch.setattr(builtins, 'input', lambda *args, **kwargs: 'Yes, I like monkeypatching')

So I would refactor the code you posted in your own answer its update to (Assuming that commands is callable, since you specified it as a side_effect):

# don't forget the imports
import builtins
import io
import sys

@pytest.mark.parametrize("m, n, commands, expec",
    helpers.get_external_inputs_outputs('spampath',helpers.read_input_output))
def test_QueryProcessor_mockpatch(monkeypatch, m, n, commands, expec):

    commands.insert(0,n)

    mock_stdout = io.StringIO()
    monkeypatch.setattr(builtins, 'input', lambda description: commands())
    monkeypatch.setattr(sys, 'stdout', mock_stdout)

    proc = hash_chains.QueryProcessor(m)
    proc.process_queries()

    assert mock_stdout.getvalue().split('n')[:-1] == expec
Answered By: Eric M.