Batsを使って手軽にCLIプログラムのテストをする

More than 3 years have passed since last update.


Bats

BatsはCLIで実行するUNIXプログラムのテストをするためのツールです。

Bash Automated Testing SystemでBatsとのこと。

https://github.com/sstephenson/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


テストの実行

では、テストを実行してみます。

スクリーンショット 2014-09-06 21.07.26.png

$ ./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