如何测试Click CLI中subprocess输出至stderr?解决测试报错
问题分析与解决方法
原因说明
这不是Click的Bug,而是CliRunner的设计限制导致的。当使用mix_stderr=False时,CliRunner会用无文件描述符(fileno)的内存缓冲区替换sys.stderr,目的是轻量级捕获输出。但subprocess.run在重定向stdout到sys.stderr时,需要目标流具备有效的文件描述符,因此抛出io.UnsupportedOperation: fileno错误。
可行解决方法
方法1:调整CLI代码,使用Click的标准stderr输出
修改CLI逻辑,先捕获subprocess的输出,再通过Click的click.echo(..., err=True)将内容发送到stderr。这样CliRunner可以正常捕获这些输出,无需依赖底层文件描述符:
import click import subprocess @click.command() def cli(): # 捕获subprocess的stdout proc_result = subprocess.run( ["echo", "hello world"], capture_output=True, text=True ) # 通过Click将内容输出到stderr click.echo(proc_result.stdout.strip(), err=True) click.echo("result")
对应的测试代码可以保持原样,且能正常验证输出:
from click.testing import CliRunner def test_foo(): runner = CliRunner(mix_stderr=False) result = runner.invoke(cli, catch_exceptions=False) assert result.stdout.strip() == "result" assert result.stderr.strip() == "hello world"
方法2:在测试中替换sys.stderr为带fileno的文件对象
如果无法修改CLI代码,可以在测试时临时将sys.stderr替换为一个临时文件(具备有效fileno),执行测试后再恢复原stderr:
import tempfile import sys from click.testing import CliRunner def test_foo(): runner = CliRunner(mix_stderr=False) with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8') as tmp_stderr: original_stderr = sys.stderr sys.stderr = tmp_stderr try: result = runner.invoke(cli, catch_exceptions=False) # 读取临时文件中的stderr内容 tmp_stderr.seek(0) captured_stderr = tmp_stderr.read().strip() # 验证输出 assert result.stdout.strip() == "result" assert captured_stderr == "hello world" finally: # 恢复原stderr sys.stderr = original_stderr
场景兼容性说明
该场景是支持的,只是需要适配CliRunner的捕获机制。方法1更符合Click的设计规范,推荐优先使用;方法2适合无法修改CLI代码的场景,避免了完整的集成测试。
内容的提问来源于stack exchange,提问作者brandonchinn178
相关产品推荐
相关产品推荐

