How to mock stdin when using fileinput module?

Question:

I have a program that uses the Python fileinput module, and I am trying to write unittests for the main() function. They work find when using an actual file, but raise OSError: reading from stdin while output is captured when I try to pass data via stdin. What is the correct way to mock the stdin input when using fileinput?

Example my_fileinput.py:

"""
$ echo "42" | python3.8 my_fileinput.py -
answer: 42
"""

import fileinput
import sys

def main():
    for line in fileinput.input(sys.argv[1:]):
        sys.stdout.write(f"answer #{fileinput.lineno()}: {line}")

if __name__ == "__main__":
    main()

Example test_my_fileinput.py:

"""
$ python3.10 -m pytest test_my_fileinput.py
OSError: reading from stdin while output is captured
"""

import io
from unittest import mock

import my_fileinput

def test_stdin():
    """Test fileinput with stdin."""

    with mock.patch.object(my_fileinput, "raw_input", create=True, return_value="42"):
        with mock.patch("sys.stdout", new=io.StringIO()) as stdout:
            with mock.patch("sys.argv", ["my_fileinput.py", "-"]):
                # Raises OSError: reading from stdin while output is captured
                my_fileinput.main()
                assert stdout.getvalue() == "answer #1: 42"

I have tried various ways of mocking stdin, all with the same results. All result in the same OSError.

Asked By: tschutter

||

Answers:

Update: A different version patching sys.stdin instead of inputfile.input

import io

from unittest import mock

import my_fileinput

def test_stdin():
    """Test fileinput with stdin."""

    with mock.patch("sys.stdin", new=io.StringIO("42n")):
        with mock.patch("sys.stdout", new=io.StringIO()) as stdout:
            with mock.patch("sys.argv", ["my_fileinput.py", "-"]):
                my_fileinput.main()
                assert stdout.getvalue() == "answer: 42n"

Warning: The original answer gets rid of the OSError, but renders other functions in the inputfile module unusable (see comments).

Original:
Changing the first two arguments of mock.patch.object to fileinput and "input" seems to fix the OSError.

with mock.patch.object(fileinput, "input", create=True, return_value="42"):

The first argument is the target object you want to patch, which is the fileinput module. The second argument is the attribute to be changed in target object, which is input.

import io
import fileinput
from unittest import mock

import my_fileinput

def test_stdin():
    """Test fileinput with stdin."""

    with mock.patch.object(fileinput, "input", create=True, return_value="42"):
        with mock.patch("sys.stdout", new=io.StringIO()) as stdout:
            with mock.patch("sys.argv", ["my_fileinput.py", "-"]):
                my_fileinput.main()
                assert stdout.getvalue() == "answer: 42n"
Answered By: Fractalism

It is not necessary for you to test fileinput itself, since that will be tested by CPython’s own test suite: Lib/test/test_fileinput.py. The pytesthonic way to test your code would be like this, using fixtures:

import my_fileinput

def test_stdin(mocker, capsys):
    mocker.patch("fileinput.lineno", return_value=1)
    mocker.patch("fileinput.input", return_value=["42n"])
    my_fileinput.main()
    out, err = capsys.readouterr()
    assert out == "answer #1: 42n"

The capsys fixture is included with pytest, and the mocker fixture is provided by plugin pytest-mock.

Answered By: wim