BatsでシェルスクリプトやCLIプログラムのテストをする

恥ずかしながら今まではシェルスクリプトで書いたプログラムのテストをするときは、echoprint-xオプションを駆使して行なっていたのですが、Batsというテストフレームワークがあることを知り早速使ってみました。

2017.9.7修正 コード他

Bats概要

Bashで動作するソフトウェアのテストフレームワークでBashで書かれています。テスト対象は、BashスクリプトだけでなくUnix上で動作するプログラムなら何でも可能なようです。というのはソースを見ると分かりますが、BatsはBashから任意のプログラムを実行しその結果の正当性(終了ステータスや出力)を検証できるような作りになっています。

早速インストールしてテストコードを書いてみます。MacでHomeBrewを使っている場合は、以下でインストール可能です。

brew install bats

実際にテストコードが動作する様子を見てみます。

#!/usr/bin/env bats

@test "1+1 should equal to 2" {
  [ "$((1+1))" -eq 2 ]
}

@test "When test.sh is passed 'hoge', it should output 'hoge'" {
  run ./test.sh "hoge"
  [ $status -eq 0 ]
  [[ $output =~ hoge ]]
}

@test "When test.sh doesn't have arguments, it should show usage" {
  run ./test.sh
  [ $status -eq 1 ]
  local usage="usage: test.sh <message>"
  [[ $output =~ $usage ]]
}

@test "Whey say function is passed 'hello', it should output 'hello'" {
  source ./functions.sh
  run say "hello"
  [ $status -eq 0 ]
  [ "${lines[0]}" = "hello" ]
}
#!/usr/bin/env bash

if [ $# -lt 1 ]; then
  echo "usage: test.sh <message>"
  exit 1
fi

echo "$1"
#!/usr/bin/env bash

function say() {
  echo "$1"
}
 ✓ 1+1 should equal to 2
 ✓ When test.sh is passed 'hoge', it should output 'hoge'
 ✓ When test.sh doesn't have arguments, it should show usage
 ✓ Whey say function is passed 'hello', it should output 'hello'

4 tests, 0 failures

シンタックス

テストコードのシンタックスは以下のようなになります。body部分はBashスクリプトとして実行されます。

# OK
@test "message" {
  # <body> bash script
}

# NG
@test "message
foo" {
  # @testに続く文字列は@testと同じ行でなければならない
}

# NG
@test "hoge"
{
  # { は@testと同じ行でなければならない
}

# NG
@test "message"{
  # "message"の前後にはスペースが必要
}

setup、teardown

XUnitフレームワークのように、各テストブロック実行前後にsetup、teardownで定義された関数を実行することができます。

setup() {
  mkdir ./tmp
}

teardown() {
  rm -rf ./tmp
}

run

テストブロックの中では、runという関数が使えます。runは、任意のスクリプトやコマンド、関数を指定することができます。終了ステータスは$status、標準出力・標準エラー出力の結果は$outputという変数で参照できます。また、$linesという変数には改行区切りで配列参照が可能です。

@test "inline code" {
  run echo "hoge"
  [ "$output" = "hoge" ]
}

@test "function" {
  run say "hello"
  [ "$output" = "hello" ]
}

@test "script" {
  run ./test.sh "hoge"
  [ "$status" -eq 0 ]
  [ "$output" = "hoge" ]
}

skip

skipを使うと後続のコマンドを実行せずに次のテストに移ります。

@test "skip" {
  skip
  false
}

@test "executed" {
  true
}

実行すると以下のようになります。

$ bats skip.bats 
 - skip (skipped)
 ✓ executed

2 tests, 0 failures, 1 skipped

load

load関数を実行すると、任意のスクリプトをsourceコマンドで読み込みます。拡張子は、.bashで指定する必要があります。

include() {
  [[ $output =~ $1 ]]
}
setup() {
 load test_helper
}

@test "include 'hoge'" {
 run echo "hoge"
 include "hoge"
}

どのように実行されている?

test.batsのコードは、実際には以下のようなシェルスクリプトに変換されて実行されているようです。$TMPDIRを見ると実際のスクリプトを確認することができます。

#!/usr/bin/env bats

test_1-2b1_should_equal_to_2() { bats_test_begin "1+1 should equal to 2" 3; 
  [ "$((1+1))" -eq 2 ]
}

test_When_test-2esh_is_passed_-27hoge-27-2c_it_should_output_-27hoge-27() { bats_test_begin "When test.sh is passed 'hoge', it should output 'hoge'" 7; 
  run ./test.sh "hoge"
  [ $status -eq 0 ]
  [[ $output =~ hoge ]]
}

test_When_test-2esh_doesn-27t_have_arguments-2c_it_should_show_usage() { bats_test_begin "When test.sh doesn't have arguments, it should show usage" 13; 
  run ./test.sh
  [ $status -eq 1 ]
  local usage="usage: test.sh <message>"
  [[ $output =~ $usage ]]
}

test_Whey_say_function_is_passed_-27hello-27-2c_it_should_output_-27hello-27() { bats_test_begin "Whey say function is passed 'hello', it should output 'hello'" 20; 
  source ./functions.sh
  run say "hello"
  [ $status -eq 0 ]
  [ "${lines[0]}" = "hello" ]
}


bats_test_function test_1-2b1_should_equal_to_2
bats_test_function test_When_test-2esh_is_passed_-27hoge-27-2c_it_should_output_-27hoge-27
bats_test_function test_When_test-2esh_doesn-27t_have_arguments-2c_it_should_show_usage
bats_test_function test_Whey_say_function_is_passed_-27hello-27-2c_it_should_output_-27hello-27

ドキュメントにもあるように、.batsファイルのスクリプトはn+1回評価されます(n+1回、sourceコマンドで読み込みされます)。まず、上記の変換したファイルがsourceコマンドで読み込まれ、続いてbats_test_functionで実行するテスト関数が収集されます。この時、テスト関数は実行されません。続いて、bats_test_functionで登録された各テスト関数がsourceコマンドで読み込みされlibexec/bats-exec-testで実行されます。関数ごとにbats-exec-testコマンドを実行しますので、ある関数で設定した変数は、別の関数では参照できませんし、あるテストの設定が他のテストに影響しません。グルーバルな変数を使いたい場合は、@test{}の外に書く必要があります。

実際に確認してみます。echo "hoge"は、3回評価されるはずです。

echo "hoge" >> output.txt

setup() {
  echo "setup ${BATS_TEST_NAME}" >> output.txt
}

teardown() {
  echo "teardown ${BATS_TEST_NAME}" >> output.txt
}

@test "test" {
  true
}

@test "test2" {
  true
}

これを実行すると、以下のような出力が得られます。テスト関数+1回分評価されていることが確認できました。

$ cat output.txt 
hoge
hoge
setup test_test
teardown test_test
hoge
setup test_test2
teardown test_test2

その他

何故か以下のようにダブルブラケットを連続して書いた場合、最後の式以外の結果が打倒でないにも関わらずテストをパスしてしまいました。。原因は追えていないので何とも言えませんが、とりあえず&&で対処してみました。
(2017.9.7 修正  “”の記述が不要でした) 2017.10.2

@test "this test should be failed" {
  run echo "hoge"
  [[ $output =~ foo ]]
  [[ $output =~ hoge ]]
}

@test "this test should be failed2" {
  run echo "hoge"
  [[ $output =~ foo ]] &&  [[ $output =~ hoge ]]
}

# -----------------------------------
# Results
 ✗ this test should be failed
   (in test file test.bats, line 3)
     `[[ $output =~ foo ]]' failed
 ✗ this test should be failed2
   (in test file test.bats, line 8)
     `run echo "hoge"' failed

2 tests, 2 failures

参考リンク

  • https://github.com/sstephenson/bats
  • http://qiita.com/5t111111/items/c4a382c7dd896c353d03
入門bash 第3版
入門bash 第3版

posted with amazlet at 16.05.07
Cameron Newham Bill Rosenblatt
オライリージャパン
売り上げランキング: 119,456

関連

Batsに影響を受けているのでインタフェースは似ていますが、実行アプローチは異なるテストツールを書きました。BAsh Unit test ToolでBaut(バウト)です。

byebyehaikikyou

日記やIT系関連のネタ、WordPressに関することなど様々な事柄を書き付けた雑記です。ITエンジニア経験があるのでプログラミングに関することなどが多いです。

シェアする

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントする

Translate »