bats-assertの使用例
Table Of Contents
1. はじめに
Batsのヘルパーライブラリー bats-assert の実用的な使い方を紹介します。
テストコード例示していますので、実際に値を変えながら試してみてください。
補足情報はかなり長くなってしまったので4-1. はじめの補足に纏めました。興味を持った方はそちらをご覧下さい。
1-1. Batsコンポーネントのインストール先
ここでは Batsのコンポーネントはそれぞれ以下にインストールした環境を使用しています。
Name | Destination |
---|---|
bats-core | /usr/local |
bats-assert | /usr/local/lib |
bats-support | /usr/local/lib |
適宜、ご自身の環境に合わせて読み替え/書き換えをしてください。
2. 基礎編
テストスクリプトコードを以下に示します。以降、このコードを一部分づつ、説明する形を取ります。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
# TARGET=$(target_path $BATS_TEST_FILENAME)
# load $TARGET
@test "Exit code" {
run true ; assert_success
run false ; assert_failure
run false ; assert_failure 1
function ret33(){
return 33
}
run ret33 ; assert_failure 33
}
@test "Exit code(refute)" {
run true ; refute [ $status -eq 1 ]
run false ; refute [ $status -eq 0 ]
function ret33(){
return 33
}
run ret33 ; refute [ $status -eq 22 ]
}
@test "STDOUT: Single line" {
run echo ; assert_output ''
run echo AAAAAA ; assert_output 'AAAAAA'
run echo A B C ; assert_output 'A B C'
run echo 'ERROR: no such file or directory'
assert_output --partial 'no such'
run echo 'Foobar v0.1.2'
assert_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
# refute(not equal)
run echo ; refute_output '_'
run echo AAAAAA ; refute_output 'AAAAAb'
run echo A B C ; refute_output 'A B c'
run echo 'ERROR: no such file or directory'
refute_output --partial 'SUCCESS'
}
@test "STDOUT: Multi line" {
run cat <<-STDIN
1
22
333
44 4
STDIN
assert_line 333
refute_line foo
assert_line --index 0 '1'
assert_line --index 1 '22'
assert_line --index 2 '333'
assert_line --index 3 '44 4'
assert_equal "${lines[0]}" '1'
assert_equal "${lines[1]}" '22'
assert_equal "${lines[2]}" '333'
assert_equal "${lines[3]}" '44 4'
}
2-1. 戻り値のテスト: assert_success, assert_failure
戻り値のアサーションです。
@test "Exit code" {
run true ; assert_success
run false ; assert_failure
run false ; assert_failure 1
function ret33(){
return 33
}
run ret33 ; assert_failure 33
}
assert_failureは引数を指定しなければ0以外かをチェックします。
ここでは成功($?==0
)にtrue
コマンド、失敗($?==1
)にfalse
コマンドを使っています。また、任意の戻り値を示すために33で返すret33関数を定義して使っています。
尚、run command;
で繋いで同一行にアサーションを書くのは好みや賛否両論あるかと思いますが、単純なテストで横に長くなりすぎず、縦にバリエーションが続く場合は、実行(入力)とアサーションが並んでいる方が分かりやすくなるのでこのように書いています。
後ろの方では分けて書いたテストも出てきます。
読み易さ、書き易さに応じて使い分けるのが良いかと思います。
これらは直接assert_equal $status -eq 0
の様にもかけますが、assert_success
とassert_failure
を使った書き方の方がExit Codeに関するテストだと明確になるので良いでしょう。
2-2. 否定する戻り値のテスト: refute
@test "Exit code(refute)" {
run true ; refute [ $status -eq 1 ]
run false ; refute [ $status -eq 0 ]
function ret33(){
return 33
}
run ret33 ; refute [ $status -eq 22 ]
}
ここでは、1-1. Batsコンポーネントのインストール先に対して、否定する書き方を示しました。
但し、網羅性の為に記載しましたが、実用する機会は殆どありません。また、分かりにくいので使うのを避けるべきです。
2-3. 標準出力のテスト(1): assert_output, refute_output
ここでは、標準出力の結果が一行の場合のアサーションについて書きます。
@test "STDOUT: Single line" {
run echo ; assert_output ''
run echo AAAAAA ; assert_output 'AAAAAA'
run echo A B C ; assert_output 'A B C'
run echo 'ERROR: no such file or directory'
assert_output --partial 'no such'
run echo 'Foobar v0.1.2'
assert_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
# refute(not equal)
run echo ; refute_output '_'
run echo AAAAAA ; refute_output 'AAAAAb'
run echo A B C ; refute_output 'A B c'
run echo 'ERROR: no such file or directory'
refute_output --partial 'SUCCESS'
}
assert_output
は標準出力/標準エラー出力の結果のアサーションで、オプションが無ければ完全一致するかをチェックします。
--partial
は出力結果に特定の文字列が含まれるか、--regexp
は正規表現でのチェックです。正規表現は拡張正規表現(ERE)が使用でき、部分一致です。(全体としてマッチをテストしたい場合は例示した様にアンカー^
, $
を含めた正規表現を指定します。)
refute_output
はassert_output
の否定版です。
2-4. 標準出力のテスト(2): assert_line, refute_line
標準出力の結果が複数行の場合のアサーションについて紹介します。
@test "STDOUT: Multi line" {
run cat <<-STDIN
1
22
333
44 4
STDIN
assert_line 333
refute_line foo
assert_line --index 0 '1'
assert_line --index 1 '22'
assert_line --index 2 '333'
assert_line --index 3 '44 4'
assert_equal "${lines[0]}" '1'
assert_equal "${lines[1]}" '22'
assert_equal "${lines[2]}" '333'
assert_equal "${lines[3]}" '44 4'
}
ここではcatとヒアドキュメントで4行の文字列を出力し、各パターンのアサーションを例示しています。
assert_line
は引数を付けない場合、標準出力の行のいずれかに該当の文字列があるかをチェックします。(その否定版がrefute_line
です。)
--index n
の様に特定の行の値もチェックできます。これは別の書き方で、${lines[i]}
をチェックする書き方もできます。
3. 発展編
3-1. 特定のテストだけを動かす
Batsへ-f(--filter) <regex>
を指定することで、特定のテストだけを実行することが出来ます。
前述のテストスクリプトexample_assertions01_basic.bats
を使って、実際に幾つかのパターンを例示します。
3-1-1. 指定なし(全テストの実行)
$ ./example_assertions01_basic.bats
example_assertions01_basic.bats
✓ Exit code
✓ Exit code(refute)
✓ STDOUT: Single line
✓ STDOUT: Multi line
4 tests, 0 failures
3-1-2. 「Exit code」を含むテストの実行
$ ./example_assertions01_basic.bats -f 'Exit code'
example_assertions01_basic.bats
✓ Exit code
✓ Exit code(refute)
2 tests, 0 failures
3-1-3. 「Exit code」で終わるテストの実行
$ ./example_assertions01_basic.bats -f 'Exit code$'
example_assertions01_basic.bats
✓ Exit code
1 test, 0 failures
テストの名前は実行結果の表示だけでなく、この様に指定をする場合もありますので、ある程度分かりやすく、特定しやすい名前にするのが良いでしょう。
3-2. テストをスキップする
Batsはskip
コマンドによってテストを実行しない様に出来ます。
例えばLinuxだったら実行したい(Linux以外ならテストしないで欲しい)とか、特定のホスト名の場合ならテストを実行しない様にしたい場合などで役立ちます。
まず、skip
の使い方を見ます。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
@test "my_test" {
skip 'foo bar'
run true ; assert_success
run false ; assert_failure 1
}
実行例
$ chmod +x example_assertions0b_skip01.bats
$ ./example_assertions0b_skip01.bats
example_assertions0b_skip01.bats
- my_test (skipped: foo bar)
1 test, 0 failures, 1 skipped
skip
コマンドに引数を与えると、実行時にその値がレポートされます。
実際の使い方ですが、ある条件によってskipコマンドを実行する様に、条件式とskipコマンドを組み合わせて使います。以下、幾つかの想定される用途を見ていきます。
- Linuxなら実行する(Linuxでない場合はスキップ)
- 特定のLinuxディストリビューションならテストを実行する
- 特定のホスト名ならテストを実行する
- コマンドが利用可能ならテストを実行する
- AWS EC2ならばテストしない
3-2-1. Linuxなら実行する(Linuxでない場合はスキップ)
これはuname
コマンドで実現できます。ユースケースとして、例えば私はWindows 10のgit-bashとLinux(RHEL)を使用して、どちらでもBashとBatsテストが実行できる様にしています。
Windowsで動かすgit-bashは昔のCygwinなど昔WindowsでUnix/LinuxのCLI環境を動かしたい頃と比べ、簡単で安定して動作します。(昔は日本語の文字化けなど動いたものの動作が期待通りならない場合があったりしたものですが、今はGitクライアントを入れるだけで基本的なLinuxのCLIが動きます。)
とは言え、実際のLinuxではないので幾つかのコマンドは使えなかったり、テストが出来ない(実行すると失敗する)場合があります。
この場合は、Linuxではない場合ならskipコマンドを実行する様にテストを書きます。
以下はLinuxかどうかの判定を他のテストでも使う想定で関数化しています。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
function is_linux(){
if [ $(uname) == Linux ] ; then
return 0
fi
return 1
}
@test "my_test" {
is_linux || skip 'Not Linux'
run true ; assert_success
run false ; assert_failure 1
}
3-2-2. 特定のLinuxディストリビューションならテストを実行する
例えばRHELであれば/etc/redhat-release
ファイルがあります。また、基本的には/etc/os-release
ファイルにそれぞれの、Linuxディストリビュートごとの情報が書かれていますので、これらを判断してテストの実行とスキップをコントロール出来ます。
ただし、イレギュラーな環境の場合/etc/os-release
ファイルがないケースもあるので、そのチェックもした方が良いかと思います。(例えば、git-bashの環境にはこのファイルはありません)
3-2-3. 特定のホスト名ならテストを実行する
ホスト名はuname -n
, hostname
, hostnamectl
, $HOSTNAME環境変数から判断できます。私は経験上、uname -n
を好んで使用します。
例えばwebサーバだったらテストをしたい場合を例示してみます。ここでホスト名はweb[0-9][0-9].example.com
というルールだったとします。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
function is_websrv(){
local name=$(uname -n)
if [[ $name =~ ^web[0-9][0-9]\.example\.com$ ]] ; then
return 0
fi
return 1
}
@test "my_test" {
is_websrv || skip "Not a Web Server: $(uname -n)"
run true ; assert_success
run false ; assert_failure 1
}
ここでは、skip
コマンドのところを一工夫して、スキップ時のメッセージにホスト名を含めるようにしました。
実行結果
$ uname -n
rhel94.example.com
$ ./example_assertions03_2_skip04.bats
example_assertions03_2_skip04.bats
- my_test (skipped: Not a Web Server: rhel94.example.com)
1 test, 0 failures, 1 skipped
3-2-4. コマンドが利用可能ならテストを実行する
コマンドが使えれば(インストールされていれば)テストを実行し、それ以外の場合はskipする場合です。コマンドの利用可否はBashのtype
コマンドの戻り値から判別するのが簡便です。($PATHは通っている前提としています)
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
function has_command(){
type "$1" >/dev/null 2>&1
return $?
}
@test "my_test" {
has_command nc || skip 'The nc command is not installed.'
run true ; assert_success
run false ; assert_failure 1
}
has_command
関数でtype
コマンドの標準出力/標準エラー出力を捨てて、戻り値をそのままリターンします。(return文も端折れますが、ここでは明示しています。)
呼び出し側はnc
と言うコマンドがあるかどうかを例示しています。このコマンドがあるならばテストを実行し、なければスキップします。
3-2-5. AWS EC2ならばテストする
AWS EC2の環境かどうかを判断する例です。AWSの環境とオンプレミス環境扱う方なら、この様な判断をしたいこともあるのではないでしょうか。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
function is_aws_ec2(){
local manufacturer=${1-$(dmidecode -s system-manufacturer)}
if [ "$manufacturer" == 'Amazon EC2' ] ; then
return 0
fi
return 1
}
@test "my_test" {
is_aws_ec2 || skip 'Not an AWS EC2 instance.'
run true ; assert_success
run false ; assert_failure 1
}
local manufacturer=${1-$(dmidecode -s system-manufacturer)}
と引数受けしている理由はこの関数自体をテストできる様にしたギミックです。詳細は3-5. ファイル更新するテストで説明します。
これは、その前までの関数is_linux
, has_command
, is_websrv
では割愛しましたがこれらにも考え方は利用できます。
dmidecode -s system-manufacturer
はAWS EC2で動くLinuxならばAmazon EC2
の結果が返りますのでこれを利用します。
この様に、特定の環境(サーバ、マシン)の場合はテストを実行するかどうかは条件とskipコマンドの組み合わせ方次第です。
テストスクリプトを環境に合わせてそれぞれ準備すると、管理が煩わしいので1つのスクリプトだけになるのが好ましいかと思います。これを実現する手法としてskipコマンドの使用例を紹介しました。
3-3. 標準エラー出力のテストと一時ファイルの利用
2. 基礎編では、便宜上、assert_output
、assert_line
のアサーションは標準出力を扱うと記載しましたが、正確には標準出力と標準エラー出力を扱います。
その為、作りたい機能が標準出力と標準エラー出力エラーを使い分け、明確にどちらのストリームの文字列かをテストしたい場合に問題になります。
ですが、Batsでテストできなくはありません。ひと工夫することでテストできますので、例示するコードで紹介します。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
@test "STDERR" {
function write_stdout_stderr(){
echo $1
echo $2 1>&2
}
run write_stdout_stderr 1 22
assert_line --index 0 '1'
assert_line --index 1 '22'
tempfile=$( mktemp --tmpdir test_stderr.tmp.XXX )
write_stdout_stderr 1 22 1>/dev/null 2>$tempfile
assert_equal $? 0
run cat $tempfile
assert_output '22'
rm -f $tempfile
}
このテストでは関数write_stdout_stderr
で引数1を標準出力へ、引数2を標準エラー出力へ書き出すスタブを使います。
この関数をrun write_stdout_stderr 1 22
の様に実行すると、assert_line --index n
で標準出力も標準エラー出力もパスすることが分かります。
(標準エラー出力もassert_line
での対象になっています)
ここで標準エラー出力の結果だけを判断したいとします。
run write_stdout_stderr 1>/dev/null
の様に標準出力の結果を捨てればよさそうに思えるかもしれませんがこれはうまくいきません。
run
で実行したコマンドはその時点で標準出力と標準エラー出力を評価するための変数(${lines[n]}
)へ格納されるためです。
では、どうしたらよいか?ですが、runを使用せず、一時ファイルを利用して標準出力の結果を書き出します。
ここで、一時ファイルの扱いに関して少し触れたいと思います。
一時ファイルはmktemp
コマンドを利用して、安全に(衝突しない)ファイルが作れます。また長期間アクセスがない場合に自動的に削除されるディレクトリへ作成できます。
更に、テストの最後にはファイルを消すようにしますが、テストが失敗するとそのロジックが通らないため残存します。この時、ファイル名からどのテストで作られたのかわかるように文字列をファイル名に付与できます。
ここでは、test_stderr.tmp.XXX
の様にSTDERRのテストと分かるような名前を付与し、これをシェル変数$tempfile
で扱います。
テストに戻ります。write_stdout_stderr 1 22 1>/dev/null 2>$tempfile
で、標準出力は破棄し、標準エラー出力を一時ファイルに書いています。
次に、念のため、assert_equal $? 0
で戻り値のチェックをしています。ここでは、runで関数を呼び出していないため、$staus
にExit Codeがセットされないため、直接$?
をチェックしています。
次にrun cat $tempfile
で一時ファイルを読んで標準出力へ書き出します。その結果をassert_output '22'
でチェックしています。
最後に、rm -f $tempfile
で一時ファイルを削除します。
このように、runを使わずに関数を実行して、一時ファイルに結果を記録してその結果を評価することでストリームごとの結果をチェックできます。
3-4. 長いデータ量の出力テスト
出力データ量が長い機能の場合、結果を全てチェックするのは困難になります。
(出来なくはないですが、全量のデータをアサーションでチェックするのはテストコードに期待値のデータを全て書くことになりテストスクリプトの見通しが悪くなります。)
この場合、ハッシュ値でチェックすれば短いコードで確認できます。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
@test "create_data" {
function create_data(){
seq 1 1024
return 0
}
tempfile=$( mktemp --tmpdir test_create_data.tmp.XXX )
create_data > $tempfile
assert_equal $? 0
run md5sum -c <<<"45070a9eae28b02bec752e334781359c $tempfile"
assert_output "$tempfile: OK"
rm -f $tempfile
}
ここではスタブとしてcreate_data
関数で1024行のデータを生成し、その結果をチェックしたいとします。
これも3-3. 標準エラー出力のテストと一時ファイルの利用の様に一時ファイルを利用します。
create_data > $tempfile
で、一時ファイルに関数の結果を書き出し、念のため戻り値0を確認します。
その次に、run md5sum -c <<<"45070a9eae28b02bec752e334781359c $tempfile"
でmd5sum
コマンドを標準入力で読み込んで期待値かチェックします。
md5sum
は期待値と異なる場合、戻り値0以外を返すので、そのようなチェックでも構いませんが、今回は標準出力の結果をアサーションに掛けました。
ハッシュ値チェックとしてMD5を扱うmd5sum
を利用しましたが、Linuxではよりビット長の長いsha224sum
, sha384sum
, sha512sum
などハッシュ値計算するコマンドもあります。
好みのものを使用して下さい。おそらくデータ量次第ですが、基本的にこのような利用であればMD5アルゴリズムで充分かと思います。
期待する値自体は今回の場合は以下の様に取得できます。
$ seq 1 1024 | md5sum
45070a9eae28b02bec752e334781359c -
実際の関数の場合は一度、一時ファイルに出力するなり単実行した結果をmd5sumにかけてハッシュ値を得ます。
その際、出力結果のデータの妥当性は注意するようにしてください。
このテストだと、「要するに期待通りの結果か?」というニュアンスのテストになります。
違った場合の検出はできますが、その場合、なにがどうおかしいかは分からないので、おそらく、正常なデータと、今回の出力データを比較しながらおかしいところを特定していくことになります。
そういった意味では、ユニットテストより後のテストや回帰テスト(リグレッションテスト)のような毛色のチェックの仕方です。
できるならば、もっと細かい単位でチェック/テストが書かれていてそこでエラーが検出できるようにテストを書く方が好ましいです。
ですが、最終的なテストとして実行する意味合いは意外と多くあるように思います。
3-5. ファイル更新するテスト
ファイルを更新する機能と対応するテストについて記載します。
ITインフラでは良くBashスクリプトを書きますが、スクリプトであるファイルを更新するロジックを書きたいことがあります。
この時、テストを書こうと思うとどのように書いていいかわからず思考停止することが、私は良くありました。
直接、対象のファイルをテストで更新するわけにはいきません。大問題になります。
ならば、テストが書けないか?というと、一工夫すると機能面でのテストはできます。
#!/usr/bin/env bats
BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load
@test "update_etc-hosts" {
function update_etc-hosts(){
local before="$1"
local after="$2"
local file="${3-/etc/hosts}"
sed -i "s/$before/$after/g" $file
return $?
}
tempfile=$( mktemp --tmpdir test_create_data.tmp.XXX )
seq 1 10 > $tempfile
run update_etc-hosts 1 2 $tempfile
assert_success
run md5sum -c <<<"d50a1cc9633515c3cac4f94381efdb67 $tempfile"
assert_output "$tempfile: OK"
rm -f $tempfile
}
このテストで例示するのは/etc/hostsを更新する機能を作りたいケースです。
update_etc-hosts
関数は引数1の文字列を引数2の文字列に書き換えたい、という機能を持たせています。
テストで実際のOSの/etc/hosts
を書き換えてしまうのは問題です。他の誰も使っていないマシンが確保できるならやりようがありますが、それでも機能面を確認するために実際のOSや他のプロダクションのファイルは更新させたくありません。
「テストでバックアップを取って書き戻す?」、いやそれでも誤ったテストを実行したら良く分からない結果になりますし、書き戻すのも煩わしいですし、何よりテストは安全・気軽に実行できるべきです。
これを一工夫して(ある程度)安全な機能とテストにします。
引数3でファイル名を取り、指定されない場合は、実際に更新したいファイルを定義するよう、シェル変数の初期値を指定する機能を利用します。
local file="${3-/etc/hosts}"
が該当行です。
なお、local
は今回の場合はなくてもよいです。関数内で名前空間を閉じるために指定しています。
ポイントは${foo-DEFAULTVALUE}
です。$fooがない場合、-で指定された以降の値がデフォルト値になります。
このシェル変数のデフォルト値を利用する関数を使うと前述の3-2. テストをスキップするで説明したそれぞれの関数もテストを書くことが出来ます。
4. 補足
4-1. はじめの補足
ztombol/bats-assert
はBats(bats-core)でアサーションを簡潔に書ける機能です。bats-coreをベースに、bats-supportと併せて使用することで、簡潔なBashスクリプトのテストコードを書くことができます。
ここでは、前半と後半の2段構成として、最初にやりたいことを軸にどの様にアサーションを書けるか、よく使うものを纏めて示したいと思います。後半ではより実際の利用に際して、Tips的なテクニックを紹介します。
ここでは(少し仰々しいですが)、前半を基礎編、後半を発展編と銘打っています。
前半と後半で例示するために示すBatsスクリプトはテストコードのスクリプトファイルだけの構成1で一通りのバリエーションを記載しました。なるべくこれを見れば一通りのやりたいことがリファレンス的に使えるように心掛けています。
前半では戻り値と標準出力(加えて標準エラー出力)をどのようにテストするかを紹介します。
後半は、幾つかの実際の利用に際してのTipsを紹介しています。具体的には標準エラー出力の扱いや、直接ファイルを書き換える関数に対してテストをどのように書けるかを紹介します。
それぞれ、なるべく実用的な説明になるように心がけました。テストは前提環境が整っていれば全てパスするはずです。
適宜、テストコードの期待値を変えてみて、実際に結果がどうレポートされるか確認してみてください。
1 実際の利用に際しては、メインのBashスクリプトとテスト用のBatsスクリプトが分離していることが好ましいかと思います。これはBatsを利用したBashスクリプトのfizzbuzzで紹介していますのでそちらをご参照ください。
4-2. あとがき
この記事では、Batsでのテストで、実用性のある書き方を紹介しました。
他にもTipsとしてテクニックは多くありますが、使用頻度が特に多いと思われるものから順に、一旦、一通りのものが書けたと思っています。
(なるべく不必要な情報を削るよう心掛けたつもりましたが、長い記事になってしまったことは申し訳なく思います。)
今迄Bashスクリプトと対応するテストを書いたことが無い方が、テストに対する興味を持つようになり、テストを書き始める切っ掛けになったならば、と思います。
また、BatsでのテストはBashスクリプトが表現豊かに記述できますので、実現が難しそうなことであっても工夫次第で実現できることが多くあります。この投稿からそのヒントや気付きを持ってもらえたならば、嬉しく思います。
ありがとうございました。