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

  • 93
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

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