1

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.

4
  • 1
    Why test that fileinput.input works correctly? Your test need only ensure that whatever fileinput.input returns is iterated over correctly. I would just mock the function itself to provide a sufficient iterable for testing. Commented Jan 27, 2023 at 20:28
  • Because my code also uses other methods from fileinput such as fileinput.lineno(). and fileinput.isstdin(). Commented Jan 27, 2023 at 21:26
  • So? What fileinput does is not as important as how your code uses what it produces. Commented Jan 27, 2023 at 21:31
  • So, shouldn't the output be expected output be "answer #1: 42" now that you have modified the code under test? Where did the #lineno go? The usage example in the docstring doesn't match the behavior of your code anymore. Commented Jan 27, 2023 at 22:55

2 Answers 2

2

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("42\n")):
        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: 42\n"

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: 42\n"
Sign up to request clarification or add additional context in comments.

6 Comments

Although it did fix the original exception, the final assert fails: AssertionError: assert 'answer: 4answer: 2' == 'answer: 42\n'. It appears that the return_value must be wrapped in io.StringIO(). So the correct mock code is: with mock.patch.object(my_fileinput.fileinput, "input", create=True, return_value=io.StringIO("42")):
@tschutter Indeed, that makes the for loop iterate over lines of text instead of characters in a string. Also, the assert doesn't need a \n at the end.
Upon further testing, this does not resolve the problem. This solution allows iteration over fileinput.input, but then none of the other fileinput functionality works such as fileinput.lineno(). This can be demonstrated by replacing the call to write() in my_fileinput.py with sys.stdout.write(f"answer #{fileinput.lineno()}: {line}").
@tschutter I updated my answer with an alternative solution at the bottom. Can you check if it works?
I tried your alternative solution and it works (with an adjustment to the final assert). Thanks!
|
1

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=["42\n"])
    my_fileinput.main()
    out, err = capsys.readouterr()
    assert out == "answer #1: 42\n"

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

7 Comments

This seems to suffer from the same issue as pointed out by @tschutter under my answer.
@Fractalism Works for me. Did you copy this code exactly, or did you use return_value="42"?
Copypasted your code into test_my_fileinput.py, replaced the sys.stdout line with sys.stdout.write(f"answer #{fileinput.lineno()}: {line}") in my_fileinput.py, and then the test fails with RuntimeError: no active input()
Why "replaced the sys.stdout line with..."? My test works on the O.P. code unmodified.
@Fractalism Edited to include the patch on lineno too. That was not in the original question, and users can't be expected to trawl through comments underneath other answers looking for updates - these need to be edited into the question.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.