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


## Bats

**Bats**はCLIで実行するUNIXプログラムのテストをするためのツールです。
**Bash Automated Testing System**でBatsとのこと。

https://github.com/sstephenson/bats

Bats自体がbashで書かれていて、特にbashスクリプトのテストに最適なようですが、出力と終了ステータスをチェックするような単純な作りなので、CLIで動作するプログラムであれば何でもテストできるでしょう。

元々、ruby-buildのテストファイル眺めてたら拡張子が`*.bats`になってて、「なんだろこれ？」と思って見たら同じ作者のBatsというツールでした。
使ってみたら結構手軽で便利だったので紹介します。

## 簡単な例

以下の例を見れば大体どんな感じかわかると思います。
`bc`, `dc`の演算結果をチェックするためのテストですね。

```bash
#!/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でインストールできます。

```console
$ brew install bats
```

ソースからインストールする場合はこちらの手順で。
`/usr/local`以下にBatsがインストールされます。


```console
$ 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]

```bash
#!/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 ]

```bash
#!/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](https://qiita-image-store.s3.amazonaws.com/0/29846/8cc79e84-fa1a-cd21-d236-decbc5c55621.png "スクリーンショット 2014-09-06 21.07.26.png")


```console
$ ./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]

```ruby
#!/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
```

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

```