How do I test that command sends subprocess output to stderr?

Viewed 92

I implemented a Click CLI that will run subprocess processes, but send their stdout to stderr, so that stdout only contains the command's specific output, e.g.

@click.command()
def cli():
  subprocess.run(["echo", "hello world"], stdout=sys.stderr)
  click.echo("result")

And I want to test that "hello world" goes to stderr and "result" goes to stdout. Specifically, if I removed the stdout=sys.stderr parameter, I want my test to fail.

def test_foo():
    runner = CliRunner(mix_stderr=False)
    result = runner.invoke(cli, catch_exceptions=False)
    assert result.stdout == "result"
    assert result.stderr == "hello world"

This doesn't work though, because it sets sys.stderr to a handle without a file descriptor, which causes subprocess to fail:

            # Assuming file-like object
>           c2pwrite = stdout.fileno()
E           io.UnsupportedOperation: fileno

Is this a Click bug, or is there a workaround, or is this just not supported? I would like to avoid writing a full integration test that calls my CLI via subprocess instead of CliRunner.

1 Answers

You can patch subprocess.run and mock the stdout option so that it responds to the fileno method.

In the POSIX standard, standard error stream file descriptor number is 2. You can have your function return that.

You must then use the capfd fixture to capture the standard error stream and perform your assertion.

import io
import subprocess
from unittest.mock import patch

from click.testing import CliRunner

from .. import main


def test_foo(capfd):
    orig_run = subprocess.run
    def patch_run(args, stdout=None):
        assert stdout, 'You must configure stdout option'
        assert isinstance(stdout, io.IOBase), 'stdout option must implemement stream interface'
        assert stdout.name == '<stderr>', 'Set stdout option to stderr'
        stdout.fileno = lambda: 2 # write to standard err fd
        rv = orig_run(args, stdout=stdout)
        return rv

    with patch.object(subprocess, 'run', patch_run):
        runner = CliRunner(mix_stderr=False)
        result = runner.invoke(main.cli, catch_exceptions=False)
        assert result.stdout == "result\n"

    captured = capfd.readouterr()
    assert captured.err == "hello world\n"
Related