はじめに
アプリケーション・テスティング! nikkieです。
Command Line Interface Creation Kit、頭文字を取ってClick。
コマンドラインツールを実装できるライブラリの1つです。
Clickのドキュメントの中にテストの書き方を見つけ、手を動かしてみました。
目次
- はじめに
- 目次
- 動作環境
- ドキュメント「Testing Click Applications」
- Basic Testing(テストの基本)
- File System Isolation(ファイルシステム分離)
- Input Streams(入力ストリーム)
- 終わりに
動作環境
- Python 3.11.4
- Click 8.1.3
- ドキュメントも8.1.xを参照します。 https://click.palletsprojects.com/en/8.1.x/
- pytest 7.4.0
ドキュメント「Testing Click Applications」
Clickで作るCLIのテストの基本として、click.testingモジュール1について解説したドキュメントです。
扱っているのは次の3つ。
- Basic Testing
- File System Isolation
- Input Streams
順番に見ていきましょう。
Basic Testing(テストの基本)
https://click.palletsprojects.com/en/8.1.x/testing/#basic-testing
click.testing.CliRunner
を使うという基本が紹介されます
- CliRunnerの
invoke
メソッドでCommandを実行できる invoke
メソッドの返り値はclick.testing.Result
型で、テストのアサーションに利用できる
テスト対象:helloコマンド
import click @click.command @click.argument("name") def hello(name): click.echo(f"Hello {name}!") if __name__ == "__main__": hello()
% python hello.py Peter Hello Peter!
helloコマンドのテスト
CliRunnerにinvokeさせましょう。
from click.testing import CliRunner from hello import hello def test_hello_world(): runner = CliRunner() result = runner.invoke(hello, ["Peter"]) assert result.exit_code == 0 assert result.output == "Hello Peter!\n"
% pytest test_hello.py ============================= test session starts ============================== platform darwin -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0 rootdir: /Users/.../testing collected 1 item test_hello.py . [100%] ============================== 1 passed in 0.01s ===============================
サブコマンドのテスト
別の例としてサブコマンドにも言及されます。
For subcommand testing, a subcommand name must be specified in the args parameter of CliRunner.invoke() method:
CliRunner.invoke
の第1引数にはGroupのcli
を渡します。
そして、第2引数(文字列のリスト)にサブコマンド名(sync)を含めたリストを渡します!
result = runner.invoke(cli, ['--debug', 'sync'])
File System Isolation(ファイルシステム分離)
https://click.palletsprojects.com/en/8.1.x/testing/#file-system-isolation
CliRunnerのisolated_filesystem
メソッドを使うことで、現在のワーキングディレクトリを新しい空のディレクトリとして扱える、とあります2。
テスト対象:catコマンド
import click @click.command() @click.argument("f", type=click.File()) def cat(f): click.echo(f.read()) if __name__ == "__main__": cat()
% python cat.py input.txt
kokoro
aki
fuka
catコマンドのテスト
isolated_filesystem
メソッドを使い、コンテキストマネージャのスコープにて、catコマンドに入力するファイルを書き込む例が紹介されています。
from click.testing import CliRunner from cat import cat def test_cat(): runner = CliRunner() with runner.isolated_filesystem(): with open("hello.txt", "w") as f: f.write("Hello World!") result = runner.invoke(cat, ["hello.txt"]) assert result.exit_code == 0 assert result.output == "Hello World!\n"
isolated_filesystem
メソッドにより、テスト実行時のワーキングディレクトリが空のディレクトリのように扱えるので、そこにhello.txtを作り、相対パスでcatコマンドに入力するわけですね。
% pytest test_cat.py ============================= test session starts ============================== platform darwin -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0 rootdir: /Users/.../testing collected 1 item test_cat.py . [100%] ============================== 1 passed in 0.01s ===============================
私はテストに使うデータ(フィクスチャ)として用意したい
好みだと思いますが、私はテスト実行中に書き込むよりは、あらかじめテストに使うデータとして用意しておきたいです。
from pathlib import Path from click.testing import CliRunner from cat import cat def test_cat(): test_root_dir = Path(__file__).resolve().parent input_path = test_root_dir / "fixtures" / "cat_input.txt" runner = CliRunner() result = runner.invoke(cat, [str(input_path)]) assert result.exit_code == 0 assert result.output == "Hello World!\n"
どちらの方法にもメリット・デメリットはあると思います。
デメリット < メリットとなる方法を使うのがよいと思います(私の文脈ではテストに使うデータとして用意するほうがデメリットが小さそう)
- テストに使うデータ(フィクスチャ)として用意する
- メリット:テストに使うデータが増えても、テストメソッドは簡潔なままで済む
- デメリット:フィクスチャのパスの取得が(私にとってはボイラープレートではあるが)easyではない
isolated_filesystem
メソッドを使う
また、コマンドが出力するファイルについては、isolated_filesystem
よりも、pytestのフィクスチャ(tmp_path3)を使えばよさそうと思いました。
Input Streams(入力ストリーム)
https://click.palletsprojects.com/en/8.1.x/testing/#input-streams
入力にはinvoke
メソッドのinput
引数が使えます。
テスト対象:promptコマンド
import click @click.command() @click.option("--foo", prompt=True) def prompt(foo): click.echo(f"foo={foo}") if __name__ == "__main__": prompt()
optionのprompt=True
については以下をどうぞ:
https://click.palletsprojects.com/en/8.1.x/options/#prompting
% python prompt.py Foo: wau wau foo=wau wau
「Foo: 」と促されるので「wau wau」と入力しました。
promptコマンドのテスト
from click.testing import CliRunner from prompt import prompt def test_prompt(): runner = CliRunner() result = runner.invoke(prompt, input="wau wau\n") assert result.exit_code == 0 assert result.output == "Foo: wau wau\nfoo=wau wau\n"
% pytest test_prompt.py ============================= test session starts ============================== platform darwin -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0 rootdir: /Users/.../testing collected 1 item test_prompt.py . [100%] ============================== 1 passed in 0.01s ===============================
終わりに
Clickのドキュメント「Testing Click Applications」の3つの例を見てきました。
click.testing.CliRunner
のinvoke
メソッドにコマンドやグループと、引数(文字列)のリストを渡す- サブコマンドのテストは、引数のリストでサブコマンド名を指定
isolated_filesystem
で、現在のワーキングディレクトリを空のディレクトリのように扱える- IMO:私にはあんまり使いどころないかも
- 入力するファイルは、(テストコードで書き込むのでなく)ファイルとして用意したい
- 出力するファイルは、pytestの一時ディレクトリを作るフィクスチャを使って指定したい
- IMO:私にはあんまり使いどころないかも
invoke
メソッドはinput
引数で、コマンド実行中にプロンプトで渡す値を指定できる
Clickで実装したアプリケーションのテストはどんと来い!という気持ちです。