Tak's Notebook

Kaggle, Machine Learning, Engineering

click を使ったエラーケースのテスト方法

背景

コマンドラインインターフェイスclick のテスト方法についてのメモ

実装

コード

以下のコードをテストしたい場合を考える。

# src/main.py
import click


@click.group()
def cli():
    pass


@cli.command()
@click.option("--name", type=str, required=True, help="The person to greet.")
def hello(name):
    if name == "Bob":
        raise ValueError("Sorry, Bob is not allowed.")
    click.echo(f"Hello {name}!")


@cli.command()
@click.option("--name", type=str, required=True, help="The person to greet.")
def goodbye(name):
    click.echo(f"Goodbye {name}!")
# src/__main__.py
from src.main import cli


if __name__ == "__main__":
    cli()

通常の実行例は以下のようになる想定。

python -m src hello --name Alice

テスト方法

click.testing.CliRunner

click のテストでは click.testing.CliRunner を使用する。

Testing Click Applications — Click Documentation (8.1.x)

上記の例では Bob という引数が来たらエラーになるので、そのテストをしたいとする。

Pytest でのエラーケースのテストは pytest.raises を使うので、同様のコードを書くと以下のようになる。

import pytest
from click.testing import CliRunner


def test_cli(name):
    from src.main import cli

    runner = CliRunner()
    with pytest.raises(ValueError):
        runner.invoke(cli, ["hello", "--name", "Bob"])

しかし、このテストを実行するとエラーが出る。

CliRunner.invoke 自体の実行はエラーが生じないためと思われる。

>       with pytest.raises(ValueError):
E       Failed: DID NOT RAISE <class 'ValueError'>

tests/test_main.py:9: Failed


click.testing.Result

click のテストでエラーが生じる場合のテストは CliRunner.invoke の返り値を利用する

import pytest
from click.testing import CliRunner


def test_cli():
    from src.main import cli

    runner = CliRunner()
    result = runner.invoke(cli, ["hello", "--name", "Bob"])
    assert result.exit_code != 0
    assert isinstance(result.exception, ValueError)
    assert str(result.exception) == "Sorry, Bob is not allowed."

https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/testing.py#L104-L133

以上を踏まえて、正常系と異常系のテストケースをまとめると以下のようになる。

import pytest
from click.testing import CliRunner


@pytest.mark.parametrize(
    "name, expected_error",
    [
        ("Alice", None),
        ("Bob", ValueError("Sorry, Bob is not allowed.")),
    ],
)
def test_cli(name, expected_error):
    from src.main import cli

    runner = CliRunner()
    result = runner.invoke(cli, ["hello", "--name", name])
    if expected_error:
        assert result.exit_code != 0
        assert isinstance(result.exception, ValueError)
        assert str(result.exception) == str(expected_error)
    else:
        assert result.exit_code == 0
        assert result.output == f"Hello {name}!\n"

まとめ

  • click のテストではclick.testing.CliRunner を使用する
  • 異常系のテストでは CliRunner.invoke の返り値の Result.exit_codeResult.exception を利用する