跳至内容

测试

使用 pytest 测试 Typer 应用程序非常容易。

假设你有一个应用程序 app/main.py,其中包含

from typing import Optional

import typer

app = typer.Typer()


@app.command()
def main(name: str, city: Optional[str] = None):
    print(f"Hello {name}")
    if city:
        print(f"Let's have a coffee in {city}")


if __name__ == "__main__":
    app()

因此,你可以像这样使用它

$ python main.py Camila --city Berlin

Hello Camila
Let's have a coffee in Berlin

该目录中还有一个空的 app/__init__.py 文件。

因此,app 是一个“Python 包”。

测试应用程序

导入并创建 CliRunner

创建另一个文件/模块 app/test_main.py

导入 CliRunner 并创建一个 runner 对象。

这个 runner 将“调用”或“执行”你的命令行应用程序。

from typer.testing import CliRunner

from .main import app

runner = CliRunner()


def test_app():
    result = runner.invoke(app, ["Camila", "--city", "Berlin"])
    assert result.exit_code == 0
    assert "Hello Camila" in result.stdout
    assert "Let's have a coffee in Berlin" in result.stdout

提示

文件名称必须以 test_ 开头,这样 pytest 才能自动检测到它并使用它。

调用应用程序

然后创建一个函数 test_app()

在函数内部,使用 runner调用 应用程序。

runner.invoke() 的第一个参数是 Typer 应用程序。

第二个参数是 str列表,包含您将在命令行中传递的所有文本,就像您在命令行中传递它一样。

from typer.testing import CliRunner

from .main import app

runner = CliRunner()


def test_app():
    result = runner.invoke(app, ["Camila", "--city", "Berlin"])
    assert result.exit_code == 0
    assert "Hello Camila" in result.stdout
    assert "Let's have a coffee in Berlin" in result.stdout

提示

函数名称必须以 test_ 开头,这样 pytest 就可以自动检测到它并使用它。

检查结果

然后,在测试函数内部,添加 assert 语句以确保调用结果中的所有内容都如预期。

from typer.testing import CliRunner

from .main import app

runner = CliRunner()


def test_app():
    result = runner.invoke(app, ["Camila", "--city", "Berlin"])
    assert result.exit_code == 0
    assert "Hello Camila" in result.stdout
    assert "Let's have a coffee in Berlin" in result.stdout

这里我们检查退出代码是否为 0,因为对于没有错误退出程序来说,退出代码为 0。

然后我们检查打印到“标准输出”的文本是否包含我们的 CLI 程序打印的文本。

提示

如果您的 CliRunner 实例是使用 mix_stderr=False 参数创建的,您也可以独立于“标准输出”检查 result.stderr 以获取“标准错误”。

信息

如果您需要复习“标准输出”和“标准错误”是什么,请查看 打印和颜色:“标准输出”和“标准错误” 部分。

调用 pytest

然后您可以在您的目录中调用 pytest,它将运行您的测试。

$ pytest

================ test session starts ================
platform linux -- Python 3.10, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 1 item

---> 100%

test_main.py <span style="color: green; white-space: pre;">.                                 [100%]</span>

<span style="color: green;">================= 1 passed in 0.03s =================</span>

测试输入

如果您有一个带有提示的 CLI,例如

import typer
from typing_extensions import Annotated

app = typer.Typer()


@app.command()
def main(name: str, email: Annotated[str, typer.Option(prompt=True)]):
    print(f"Hello {name}, your email is: {email}")


if __name__ == "__main__":
    app()

提示

如果可能,请优先使用 Annotated 版本。

import typer

app = typer.Typer()


@app.command()
def main(name: str, email: str = typer.Option(..., prompt=True)):
    print(f"Hello {name}, your email is: {email}")


if __name__ == "__main__":
    app()

您将像这样使用它

$ python main.py Camila

# Email: $ camila@example.com

Hello Camila, your email is: camila@example.com

您可以使用 input="camila@example.com\n" 测试在终端中输入的文本。

这是因为您在终端中输入的内容会进入“标准输入”,并由操作系统处理,就好像它是一个“虚拟文件”一样。

信息

如果您需要复习“标准输出”、“标准错误”和“标准输入”是什么,请查看 打印和颜色:“标准输出”和“标准错误” 部分。

当您在输入电子邮件后按下 ENTER 键时,这只是一个“换行符”。在 Python 中,它用 "\n" 表示。

因此,如果您使用 input="camila@example.com\n",则意味着:“在终端中输入 camila@example.com,然后按下 ENTER 键”。

from typer.testing import CliRunner

from .main import app

runner = CliRunner()


def test_app():
    result = runner.invoke(app, ["Camila"], input="camila@example.com\n")
    assert result.exit_code == 0
    assert "Hello Camila, your email is: camila@example.com" in result.stdout

测试函数

如果你有一个脚本,但从未显式创建过 typer.Typer 应用程序,例如

import typer


def main(name: str = "World"):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

...你仍然可以通过在测试期间创建应用程序来测试它

import typer
from typer.testing import CliRunner

from .main import main

app = typer.Typer()
app.command()(main)

runner = CliRunner()


def test_app():
    result = runner.invoke(app, ["--name", "Camila"])
    assert result.exit_code == 0
    assert "Hello Camila" in result.stdout

当然,如果你正在测试该脚本,直接在 main.py 中创建显式的 typer.Typer 应用程序可能更容易/更简洁,而不是只在测试期间创建它。

但如果你想保持这种方式,例如因为它是在文档中的一个简单示例,那么你可以使用这个技巧。

关于 app.command 装饰器

注意 app.command()(main)

如果它不明显它在做什么,请继续阅读...

你通常会写类似的东西

@app.command()
def main(name: str = "World"):
    # Some code here

@app.command() 只是一个装饰器。

这等同于

def main(name: str = "World"):
    # Some code here

decorator = app.command()

new_main = decorator(main)
main = new_main

app.command() 返回一个函数(decorator),它以另一个函数作为其唯一参数(main)。

通过使用 @something,你通常告诉 Python 用 decorator 函数(new_main)的返回值替换下面的内容(函数 main)。

现在,在 Typer 的特定情况下,装饰器不会更改原始函数。它在内部注册它并返回未修改的它。

所以,new_main 实际上是相同的原始 main

因此,在 Typer 的情况下,因为它实际上没有修改装饰的函数,这将等同于

def main(name: str = "World"):
    # Some code here

decorator = app.command()

decorator(main)

但我们不需要创建变量 decorator 来在下面使用它,我们可以直接使用它

def main(name: str = "World"):
    # Some code here

app.command()(main)

...就是这样。在 main.py 文件中直接创建显式的 typer.Typer 可能仍然更简单 😅。