pytest捕获cli命令的stdout/stderr应优先用capfd(支持子进程输出),配合monkeypatch模拟sys.argv,用pytest.raises(systemexit)拦截退出,并mock外部命令调用。

pytest怎么捕获CLI命令的stdout和stderr
直接用 print() 输出的内容在 pytest 里默认不显示,也不容易断言。得靠 capfd 或 capsys 这类内置 fixture 来抓取。
区别在于:capfd 捕获的是文件描述符级别的输出(包括子进程、C 扩展打印的内容),capsys 只捕获 Python 的 sys.stdout/sys.stderr。对 CLI 工具来说,尤其用了 subprocess 或调了外部命令,优先选 capfd。
- 测试前加参数
@pytest.mark.parametrize("args", [["--help"], ["-v"]])能批量测不同命令行参数 - 调用入口函数时别用
os.system()或subprocess.run(..., shell=True),容易绕过 Python 控制流,导致capfd失效 - 如果 CLI 入口是
if __name__ == "__main__": main(),测试时直接导入并调用main(),比启动新进程更可控
如何模拟 sys.argv 并避免修改全局状态
CLI 工具通常依赖 sys.argv 解析参数,但直接改它会影响其他测试用例。不能写 sys.argv = ["mytool", "--version"],得用临时替换方式。
推荐用 monkeypatch.setattr():
def test_version_output(monkeypatch, capfd):
monkeypatch.setattr("sys.argv", ["mytool", "--version"])
main() # 假设这是你的 CLI 入口
out, err = capfd.readouterr()
assert "1.2.0" in out
- 不用
import sys; sys.argv = [...],否则可能污染后续测试 - 如果工具用的是
argparse.ArgumentParser().parse_args(),也可以直接传参:main(["--version"])(前提是main()支持接收 args 列表) - 注意:某些库(如
click)的cli()函数默认只从sys.argv读,这时必须用monkeypatch或改用cli.main(args=[...], standalone_mode=False)
测试异常退出(exit code ≠ 0)时怎么不让pytest崩掉
CLI 工具遇到错误常调 sys.exit(1),这会让 pytest 测试直接中断,报 SystemExit 异常而不是失败。得提前拦截。
调用 Cutout.Pro 视觉处理 API 进行背景移除、人像抠图和照片增强,支持文件上传与图片 URL 输入。
两种可靠做法:
- 用
pytest.raises(SystemExit)显式声明预期退出,再检查退出码:with pytest.raises(SystemExit) as e: main(); assert e.value.code == 2 - 对
click工具,用cli.main(args=["--invalid"], standalone_mode=False),它不会触发真实sys.exit,而是抛SystemExit,可被捕获 - 避免在测试里用
try/except SystemExit手动吞异常——pytest 的raises更清晰且自带上下文
怎么测 CLI 调用外部命令(比如 git、curl)而不真执行
真实调外部命令会导致测试变慢、不稳定、依赖环境。必须 mock。
关键不是 mock subprocess.run 本身,而是 mock 它返回的对象(尤其是 returncode、stdout):
from unittest.mock import patch
def test_git_branch_call(capfd):
with patch("subprocess.run") as mock_run:
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = "main\n"
main(["--branch"]) # 假设这个命令会调 git branch
out, _ = capfd.readouterr()
assert "Current branch: main" in out
- 别只 mock
returncode,CLI 工具往往还解析stdout字符串,mock 时得一并设好 - 如果工具用了
subprocess.Popen,mock 对象要换成subprocess.Popen,并设置mock_instance.communicate.return_value = (b"ok", b"") - mock 路径要写对:是工具代码里 import 的位置,不是
subprocess模块本身。比如工具里写了from subprocess import run,就得 mock"mymodule.run"
真实项目里最常漏的是:没隔离 sys.argv 和没处理 SystemExit,结果测试偶尔失败或直接中断。这两处不稳,整个 CLI 测试集就不可靠。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










