Search code examples
pythonpython-3.xunit-testingmockingstdin

How to mock stdin when using fileinput module?


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.


Solution

  • 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"