baut †
Bashで書かれたテストツール。
batsのようにコマンドラインベースでプログラムの振る舞いをテストすることを目的としている。
テストプログラム自体もすべてBashであり、ツール特有のシンタックスはない。
XUnitのような感覚で使える。
インストール †
任意の場所にソース一式をダウンロードして、bin
ディレクトリにパスを通すだけである。
install.sh
を実行すれば同じことをしてくれる。
$ git clone https://github.com/moritetu/baut.git $ cd baut $ source install.sh $ baut run test
パスが通っていれば、-h
でUsageを確認できる。
$ baut -h Usage: baut [-v] [-h] [--d[0-4]] [run|<command>] [<args>] OPTIONS -v, --version Show version. -h, --help Show usage. --d[0-4] Set log level to TRACE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4) COMMANDS compile Compile a test script file. init Generates test files from a template. report Format the result of 'baut test' execution. run Run tests. test Run tests in a file and print its result. Ordinally this command is called in 'run' command. Show more available information about a specific command. 'baut <command> [-h|--help]'
テスト †
テストプログラム †
- テストファイルは、拡張子が
*.sh
である。 - テストファイル名は、
test_
で始まる必要がある。 - テスト単位は、シェルの関数であり、
test_
で始まる関数はテスト対象である。
test_
で始まる関数名でなくともアノテーション@Test
が付与されている関数はテスト対象となる。 #:
は特別な意味を持ち、#:
の後には@アノテーション
などを記述する。- ファイル内のテストは上から順に実行される。
コマンドがエラーで終了するとテストは失敗する。
失敗しても回避したい場合は、<command> || status=$?
などでステータスを一時保存しればよい。
#!/usr/bin/env bash #: @BeforeAll function setup_all() { : # 本ファイルのすべてのテスト実行前に一度呼ばれる } #: @BeforeEach function setup() { : # 各テスト前に一度呼ばれる } #: @Test(テストの内容について記述できる) test_ng_sample() { fail "Not implemented" } #: @Test test_ng_sample2() { run echo "bar" [ $status -ne 0 ] || fail "exit status should not be 0, but '$status'" "result: $result" } #: @Test test_ok_sample() { run echo "hello baut" [ "$result" = "hello baut" ] [ $status -eq 0 ] } #: @Test test_skip_sample() { run echo "hello baut" skip "Good bye!" echo "Not reach here" } #: @Test test_wait_until() { local pidfile="$(__DIR__)/sample.pid" eval "sleep 2 && echo $BASHPID > $pidfile" & wait_until --retry-max 3 "[ -e '$pidfile' ]" rm $pidfile ||: } #: @AfterEach function teardown() { : # 各テストの実行後に呼ばれる } #: @AfterAll function after_all() { : # 本ファイルのすべてのテスト実行後に呼ばれる }
実行すると以下のようになる。
$ baut r test_sample.sh 1 file, 5 tests #1 /tmp/test_sample.sh x テストの内容について記述できる Not implemented # Error(1) detected at the following: # 13 #: @Test(テストの内容について記述できる) # 14 test_ng_sample() { #=> 15 fail "Not implemented" # 16 } # 17 x test_ng_sample2 exit status should not be 0, but '0' result: bar # Error(1) detected at the following: # 19 test_ng_sample2() { # 20 run echo "bar" #=> 21 [ $status -ne 0 ] || fail "exit status should not be 0, but '$status'" "result: $result" # 22 } # 23 o test_ok_sample ~ test_skip_sample # SKIP Good bye! o test_wait_until #$ 5 tests, 2 ok, 2 failed, 1 skipped 💥 1 file, 5 tests, 2 ok, 2 failed, 1 skipped Time: 0 hour, 0 minute, 3 seconds
テストスイートの実行 †
ディレクトリを指定して実行すれば、ディレクトリ下のテストを順に実行する。
テストファイルの実行順序は決まっていない。
$ baut run testdir
2階層以上にもテストファイルが存在する場合は、-r
をつける。
$ baut run -r testdir
テスト実行前確認 †
実行されるテストについて、テスト実行前に確認できる。
#: @BeforeAll function setup_all() { : } #: @BeforeEach function setup() { : } #: @Test test_ng_sample() { fail "Not implemented" } #: @Test test_ng_sample2() { run echo "bar" [ $status -ne 0 ] || fail "exit status should not be 0, but '$status'" "result: $result" } #: @Test test_ok_sample() { run echo "hello baut" [ "$result" = "hello baut" ] [ $status -eq 0 ] } #: @Test test_skip_sample() { run echo "hello baut" skip "Good bye!" echo "Not reach here" } #: @Test test_wait_until() { local pidfile="$(__DIR__)/sample.pid" eval "sleep 2 && echo $BASHPID > $pidfile" & wait_until --retry-max 3 "[ -e '$pidfile' ]" rm $pidfile ||: } #: @AfterEach function teardown() { : } #: @AfterAll function after_all() { : }
実行される関数は以下のとおりである。
baut r -d test_sample.sh [1] /test/test_sample.sh ├─ (1) before_all_functions => setup_all ├─ (1) before_each_functions => setup ├─ (5) test_functions => test_ng_sample test_ng_sample2 test_ok_sample test_skip_sample test_wait_until ├─ (1) after_each_functions => teardown └─ (1) after_all_functions => after_all
()
内の数字は実行される数である。
特定のテストのみ実行 †
--match
オプションを使って実行する。
$ cat test_match.sh #: @Test param_test1() { test 1 -eq 1 } #: @Test param_test2() { test 1 -eq 1 } #: @Test command_test1() { run echo "hoge" [ "$stdout" = "hoge" ] } #: @Test command_test2() { run echo "foo" [ "$stdout" = "foo" ] }
実行すると以下のようになる。
# baut run --match "param_*" test_match.sh 1 file, 4 tests #1 /test/diff/test_match.sh o param_test1 o param_test2 #$ 4 tests, 2 ok, 0 failed, 0 skipped # WARNING planned tests were not executed absolutely! 🎉 1 file, 2 tests, 2 ok, 0 failed, 0 skipped Time: 0 hour, 0 minute, 0 second
ファイル内のすべてのテストが実行されていないので、WARNINGメッセージでその旨が実行される。
特徴・機能 †
コマンド †
run †
後に続くコマンドを実行し、結果を変数に格納する。
変数 | 説明 |
---|---|
result | stdoutとstderrの結果 |
status | コマンドの終了ステータス |
lines | stdoutとstderrの結果の行単位の配列 |
run2 †
後に続くコマンドを実行し、結果を変数に格納する。
runと違うのは、stdoutとstderrを別々に扱うこと。
変数 | 説明 |
---|---|
result | stdoutの結果 |
status | コマンドの終了ステータス |
lines | stdoutの結果の行単位の配列、stdout_linesと同じ |
stdout | stdoutの結果 |
stderr | stderrの結果 |
stdout_lines | stdoutの結果の行単位の配列 |
stderr_lines | stderrの結果の行単位の配列 |
myfunc() { echo "hoge" echo "bar" >&2 exit 1 } test_run() { run echo "hoge" [ "$result" = "hoge" ] [ "${lines[0]}" = "hoge" ] (( status == 0 )) } test_run2() { run2 myfunc [ "$result" = "hoge" ] [ "${stdout_lines[0]:-}" = "hoge" ] [ "${stderr_lines[0]}" = "bar" ] [ "$stdout" = "hoge" ] [ "$stderr" = "bar" ] (( status == 1 )) }
eval2 †
run2と同じ、リダイレクトやパイプなどを含むコマンドを実行する。
wait_until †
実行に時間のかかるコマンドを待ち合わせる。
test_wait_until() { local pidfile="$(__DIR__)/sample.pid" eval "sleep 2 && echo $BASHPID > $pidfile" & wait_until --retry-max 3 "[ -e '$pidfile' ]" test -e "$pidfile" && rm "$pidfile" ||: }
stop †
テストファイル内での以降のテストプロセスを停止する。
テストスイート全体で失敗時にテストを停止したい場合は、baut r -s
を実行する。
skip †
テストをスキップする。
実行しているテストがスキップされ、次のテストに移る。
fail †
テストを失敗させる。
実行しているテストは直ちに失敗となり、次のテストに移る。
test_skip() { skip "This test is skipped" echo "not reach here" } test_fail() { fail "Not implementation" } test_stop() { stop "halt test" }
_setup、_cleanup †
テスト全体で一度のみ実行したい処理は、_all.shというファイルに記述する。
これは、テスト実行ディレクトリのtopにおいておく必要がある。
_setup
、_cleanup
は、テスト実行前、テスト実行後、にそれぞれ一回のみ実行される。
複数のテストファイルがある場合、一度のみ実行される。
ディレクトリ構成例
/path/to/test_root |- all.sh # test_rootでテストをrunすれば有効 |- test_hoge.sh |- test_parameters |- test_get.sh |- test_post.sh |- _all.sh # これは実行されない、test_parametersでテストをrunすれば有効
_all.sh
#!/usr/bin/env bash # _all.sh # _setup() { echo "called at the begin of test suite" } _cleanup() { echo "called at end begin of test suite" }
_all.sh
でないファイルを指定したい場合は、baut r --wrap-script <file>
で指定可能。
このファイルは、テスト実行の最初にインクルードされる。
そのため、_setup
関数はあるものの関数の外に記述したコマンドは初回に評価される。
アノテーション †
@BeforeAll †
#: @BeforeAll
テストファイル内のテスト関数実行前に一度のみ呼ばれる。
複数定義している場合は、順に実行される。
# (1) #: @BeforeAll setup_all1() { GLOBAL_VAR1=10 } # (2) #: @BeforeAll setup_all2() { export PATH=/usr/local/bin:"$PATH" }
@BeforeEach †
#: @BeforeEach
各テスト関数実行前に逐次実行される。
複数定義がある場合は、順に実行される。
#: @BeforeEach setup1() { touch flagfile } #: @BeforeEach setup2() { TEST_VAR2=20 }
@Test †
#: @Test[(<text>)]
テスト対象関数であることを示す。
test_
で始まる関数は自動的にテスト関数と見なすが、そうでない場合でも@Test
をつければテスト対象となる。
また、テスト実行レポートに出力されるメッセージはテスト関数名であるが、<text>
を指定することでメッセージを任意の文字列に置換できる。
#: @Test(This test should be absolutely passed) test_passed() { [ 1 -eq 1 ] }
@TODO †
#: @TODO[(<text>)]
TODOであることを示す。
@TODO
のついた関数はテスト対象と見なされる。
テストレポートに# TODO <text>
というマークをつけることができる。
@Ignore †
#: @Ignore
テスト対象から除外される。
テストレポートにも表示されない。
@Deprecated †
#: @Deprecated[(<text>)]
非推奨であることを示す。
また、関数はテスト対象関数としてみなされる。
# DEPRECATED <text>
というメッセージがレポートに出力される。
@AfterEach †
#: @AfterEach
テスト実行後に逐次実行される。
#: @AfterEach teardown() { rm flagfile ||: }
@AfterAll †
#: @AfterAll
ファイル内のすべてのテスト実行後に実行される。
#: @AfterAll teardown_all() { rm "$TMPDIR/*.tmp" ||: }
@., @source, @include †
#: @.(test_a.sh) #: @.(test_b.sh) #: @.(test_c.sh)
複数のテストに分割した場合に、他のテストをインクルードする。
テスト結果は、インクルード元にカウントされる。
test_main.sh
#: @.(test_b.sh) #: @.(test_c.sh)
test_b.sh
test_b() { [ 1 -eq 1 ] }
test_c.sh
test_c() { [ 1 -eq 1 ] }
実行結果は以下のとおり。
$ baut r test_main.sh 1 file, 2 tests #1 /test/test_main.sh o test_b o test_c #$ 2 tests, 2 ok, 0 failed, 0 skipped 🎉 1 file, 2 tests, 2 ok, 0 failed, 0 skipped Time: 0 hour, 0 minute, 0 second
helper †
テストの実行を支援する機能を追加したい場合に使用する。
load
コマンドを使ってロードする。
以下のパスにあるfileが検索対象となる。
- .
- helpers/
- baut/libexec
- baut/helpers
diff-helper †
diffを使って期待結果ファイルと実行結果を比較する機能を提供する。
load "diff-helper.sh"
これにより、以下の関数が使用可能となる。
期待結果ファイルは、expected/<関数名>.out
である。
実際の結果は、results/<関数名>.out
である。
差分があった場合は、results/<関数名>.out.diff
ファイルが生成される。
run_diff †
diffの結果、失敗しても処理を継続する。
$ tree . . ├── expected │ └── foo.out ├── results └── test_foo.sh 2 directories, 2 files
foo.out
$ cat expected/foo.out bar
実行。
$ baut r test_foo.sh 1 file, 1 test #1 /test/diff/test_foo.sh o foo See /test/diff/results/foo.out.diff #$ 1 test, 1 ok, 0 failed, 0 skipped 🎉 1 file, 1 test, 1 ok, 0 failed, 0 skipped Time: 0 hour, 0 minute, 0 second
差分は以下のとおり。
$ cat /test/diff/results/foo.out.diff --- /test/diff/expected/foo.out 2020-02-16 01:34:49.390773740 +0900 +++ /test/diff/results/foo.out 2020-02-16 01:37:46.481055289 +0900 @@ -1 +1 @@ -bar +foo
run_diffx(run diff and exitの略) †
run_diff
と同じであるが、diffの結果が不一致であった場合、現在実行しているテストは終了する。
run_diff
は、とりあえず一度すべて流してdiffを一通り確認したい場合に有用。
begin_comparing、end_comparing †
begin_comparing
とend_comparing
で囲まれた範囲で実行されて出力の結果をdiffの対象とする。
複数のコマンドの実行結果をまとめて比較したい場合に有用。
test_lines() { begin_comparing { echo "line1" echo "line2" echo "line3" } end_comparing }
ユーティリティ †
__FILE__、__DIR__、__LINE__ †
ファイル、ディレクトリ、行番号を表示する。
$ tree test test └── test_util.sh 0 directories, 1 file
パスするテストは以下のようになる。
test_util() { [ `__FILE__` = "/test/test_util.sh" ] [ `__DIR__` = "/test" ] [ `__LINE__` -eq 4 ] }
self †
現在実行中の関数名を示す。
load、load_if_exists †
ヘルパーをロードする。
require †
既にロード済みの場合は、ロードしない。
resolve_link †
シンボリックリンクを解決し絶対パスを返す。
push_load_path、pop_load_path †
loadの検索対象パスに加える、削除する。
datetime †
%Y-%m-%d %H:%M:%S
の形式で日付を表示する。
log_xxx †
ログを表示する。
log_trace <message> log_debug <message> log_info <message> log_warn <message> log_error <message>
テストテンプレート †
デフォルトのテストスタイルを定義できる。
チームで統一した場合などは、templateディレクトリにひな形を用意しておくことで、initコマンドでプロジェクトを初期化できる。
$ baut init -h Usage: baut init [-t <template>] [-f] [-i] <outdir> Generates test files from a template. 'init' just copies files or directories with the specified template. OPTIONS -t, --template <template> Copies from a specified template. -i Shows available templates. -f Overwrites. -DVARNAME=VALUE Passes the specified variables to place holders.
以下のように実行する。
$ baut init -i mongo [Available Templates] default mongo mysql postgresql redis $ baut init -t mongo mongo $ tree mongo mongo/ ├── _all.sh ├── expected │ └── test_query.out ├── run-test.sh └── test_sample.sh 1 directory, 4 files
テストドキュメント †
テストファイル内に説明を加えることができる。
test_equal() { [ 1 -eq 1 ] } #=begin HELP # # This test description. # description # description # #=end HELP
baut help
コマンドで出力することができる。
$ baut help test_desc.sh This test description. description description
ヘルプブロックは、以下のように#=begin HELP
と#=end HELP
で囲まれた箇所である。
#=begin HELP # # beginとendに囲まれたブロックがヘルプブロックになる。 # テストファイル内のどこに記述してもよい。 # #=end HELP