测试
使用 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
可能仍然更简单 😅。