Bats
BatsはCLIで実行するUNIXプログラムのテストをするためのツールです。
Bash Automated Testing SystemでBatsとのこと。
Bats自体がbashで書かれていて、特にbashスクリプトのテストに最適なようですが、出力と終了ステータスをチェックするような単純な作りなので、CLIで動作するプログラムであれば何でもテストできるでしょう。
元々、ruby-buildのテストファイル眺めてたら拡張子が*.bats
になってて、「なんだろこれ?」と思って見たら同じ作者のBatsというツールでした。
使ってみたら結構手軽で便利だったので紹介します。
簡単な例
以下の例を見れば大体どんな感じかわかると思います。
bc
, dc
の演算結果をチェックするためのテストですね。
#!/usr/bin/env bats
@test "addition using bc" {
result="$(echo 2+2 | bc)"
[ "$result" -eq 4 ]
}
@test "addition using dc" {
result="$(echo 2 2+p | dc)"
[ "$result" -eq 4 ]
}
インストールのしかた
MacだったらHomebrewでインストールできます。
$ brew install bats
ソースからインストールする場合はこちらの手順で。
/usr/local
以下にBatsがインストールされます。
$ git clone https://github.com/sstephenson/bats.git
$ cd bats
$ ./install.sh /usr/local
Batsでなんかテストしてみる。
では、これから適当なコマンドを使ってテストしてみましょう。
テストするコマンドの仕様
例えばこういう仕様の check_if_file_exists.sh
っていうシェルスクリプトのコマンドがあったとします。
- ファイルの存在チェックを行うコマンド
- 引数でファイル名を指定する
- 引数が未指定や2個以上指定されているとステータス1でエラー終了
- ファイルが存在したが、ディレクトリだった場合はステータス2でエラー終了
- ファイルが存在しなかった場合はステータス3でエラー終了
- ファイルが存在した場合はステータス0で正常終了
テストの内容
Batsのテスト記法にしたがって書くと、こんな感じのテストになります。
[test-check_if_file_exists.bats]
#!/usr/bin/env bats
setup() {
mkdir test_dir
touch test_file
}
teardown() {
rmdir test_dir
rm test_file
}
@test "When no argument provided, it should fail with exit code 1 and print error message" {
run ./check_if_file_exists.sh
[ "$status" -eq 1 ]
[ "${lines[0]}" = "You should specify a file name to check as an argument." ]
}
@test "When more than 1 arguments provided, it should fail with exit code 1 and print error message" {
run ./check_if_file_exists.sh
[ "$status" -eq 1 ]
[ "${lines[0]}" = "You should specify a file name to check as an argument." ]
}
@test "When the provided file is kind of a directory, it should fail with exit code 2 and print error message" {
run ./check_if_file_exists.sh test_dir
[ "$status" -eq 2 ]
[ "${lines[1]}" = "but it seems a directory." ]
}
@test "When the provided filename does not exist, it should fail with exit code 3 and print error message" {
run ./check_if_file_exists.sh nonexistance
[ "$status" -eq 3 ]
[ "${lines[0]}" = "nonexistance does not exist." ]
}
@test "When the provided filename exist, it should end normally with exit code 0 and print message" {
run ./check_if_file_exists.sh test_file
[ "$status" -eq 0 ]
[ "${lines[0]}" = "test_file exists." ]
}
ポイント
setup と teardown
unittestではおなじみのこれらも使えます。
- setup
各テスト実行前に実行されるフック関数。
テスト用の環境設定とかに使う - teardown
各テスト実行後に実行されるフック関数。
テストの後処理とかに使う
@test
@test
に続けてテストの説明を書きます。
ブロック内にテストの本体を書きます。
run
run
の引数に実行するコマンド、つまりテスト対象を指定します。
コマンドの実行後に、実行結果の出力内容と終了ステータスを $output
, $status
といった特別な変数に格納します。
$status
コマンドの終了ステータスを格納する変数です。
$output
コマンドの出力内容を格納する変数です。
$lines
出力内容の 行ごとに格納する 配列変数です。
実際にテストするコマンド
こんなシェルスクリプトだったとします。
あえてバグを仕込んでます。
[ check_if_file_exists.sh ]
#!/usr/bin/env bash
if [ $# -ne 1 ]; then
echo "You should specify a file name to check as an argument." >&2
exit 1
fi
FILE_NAME=$1
if [ -f $FILE_NAME ]; then
echo "$FILE_NAME exists."
exit 0
elif [ -d $FILE_NAME ]; then
echo "$FILE_NAME exists," >&2
echo "but it seems a directory." >&2
exit 2
else
echo "$FILE_NAME does not exist." >&2
exit 4 # バグ。ここは本来 3 で終了すべき仕様
fi
テストの実行
では、テストを実行してみます。
$ ./test-check_if_file_exists.bats
✓ When no argument provided, it should fail with exit code 1 and print error message
✓ When more than 1 arguments provided, it should fail with exit code 1 and print error message
✓ When the provided file is kind of a directory, it should fail with exit code 2 and print error message
✗ When the provided filename does not exist, it should fail with exit code 3 and print error message
(in test file test-check_if_file_exists.bats, line 33)
`[ "$status" -eq 3 ]' failed
✓ When the provided filename exist, it should end normally with exit code 0 and print message
5 tests, 1 failure
期待通りに、あえてバグを仕込んだテストのみ失敗して、他は成功しています。
bashで実装されているので特に何の準備もなく導入できて、手軽にCLIプログラムのテストをできるので結構便利に使えます。
終
上で書いた通り、bashスクリプトに限らず、どんなCLIプログラムでも、簡単なテストなら手軽にできます。
[check_if_file_exists.rb]
#!/usr/bin/env ruby
unless ARGV.length == 1
STDERR.puts "You should specify a file name to check as an argument."
exit 1
end
file_name = ARGV[0]
if File.exist?(file_name)
if File.ftype(file_name) == 'file'
puts "#{file_name} exists."
elsif File.ftype(file_name) == 'directory'
STDERR.puts "#{file_name} exists,\nbut it seems a directory."
exit 2
else
end
else
STDERR.puts "#{file_name} does not exist."
exit 4
end
$ ./test-check_if_file_exists2.bats
✓ When no argument provided, it should fail with exit code 1 and print error message
✓ When more than 1 arguments provided, it should fail with exit code 1 and print error message
✓ When the provided file is kind of a directory, it should fail with exit code 2 and print error message
✗ When the provided filename does not exist, it should fail with exit code 3 and print error message
(in test file test-check_if_file_exists2.bats, line 33)
`[ "$status" -eq 3 ]' failed
✓ When the provided filename exist, it should end normally with exit code 0 and print message
5 tests, 1 failure