テスト
Typerアプリケーションのテストは、pytestを使用すると非常に簡単です。
例えば、以下のようなアプリケーション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
を使用してアプリケーションをinvoke
します。
runner.invoke()
の最初のパラメータはTyper
アプリです。
2番目のパラメータは、コマンドラインで渡すテキストをすべて含むstr
のlist
です。コマンドラインで渡すのとまったく同じように渡します。
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であることを確認しています。
次に、「標準出力」に出力されたテキストに、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()
は、別の関数を唯一のパラメータ(main
)として受け取る関数(decorator
)を返します。
そして、通常は@something
を使用することで、Pythonに(関数main
)以下のものをdecorator
関数(new_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
を作成する方がまだ簡単でしょう😅。