ヒトリ歩き

愚痴とかいろいろ書きます

シェルスクリプトのテストフレームワークは何がいいのか

f:id:kotapontan:20210919235517p:plain

シェルスクリプトをコーディングする機会があり、単体テストをするためにテストフレームワークを調べてみた。

シェルスクリプトにもテストフレームワークがある

JavaであればJUnitPythonにはPyUnit、C/C++であればGoogleTestのように各プログラミング言語にはテストフレームワークがあるように、シェルスクリプトにもテストフレームワークが存在する。

メジャーなテストフレームワークは以下の3つだと思う。
(私が勝手にこの3つだけだと思っているかも)

  • Bats

  • Shellspec

  • shUnit2

Bats

github.com

現在も定期的にリリースされているテストフレームワーク
bats とは、Bash Automated Testing System のこと。

ドキュメントも用意されている。

bats-core.readthedocs.io

尊敬しているカックさんもブログで記事していた。
[http://kakakakakku.hatenablog.com

Shellspec

github.com

現在もBugFixしたものがリリースされている。
BDDスタイルのテストフレームワーク
他のテストフレームワークに比べると飛び抜けて高機能。
作成者ご本人がShellspecの開発の意図や設計などについて書かれている。

qiita.com

shUnit2

xUnitのテストフレームワーク
JUnitやPyUnitと同様の手法で動作するように設計されている。
お馴染みの「assertEquals」などが用意されている。

2020年4月に2.1.8がリリースされているが、その前が2018年とほぼメンテナンスされていない。

github.com

それぞれをかじってみる

加算する関数をテスト対象として、テストを作成してみる。

#!/bin/bash

function add() {

  if [[ $# -ne 2 ]] ; then
    echo "Argument Error!!" 1>&2
    return 1
  fi

  local a
  local b
  local c
  a=$1
  b=$2

  c=$((a + b))
  echo "${c}"
  return 0

}

Bats

まずは、Batsをセットアップする。
Githubからリポジトリをクローンしてインストーラを実行するとセットアップは完了する。
インストーラのパラメータにインストール先を指定することが可能で、/usr/localにインストールしたい場合は 、install.sh のパラメータに /usr/local を指定する。
今回は自分のユーザ配下にBatsをインストールした。

$ git clone https://github.com/bats-core/bats-core.git
$ cd bats-core
$ ./install.sh $HOME

Batsのテストソースは以下のようになる。
setup関数でテスト対象の関数を実装しているシェルスクリプトのファイルをロードする。
テスト対象の関数を実行すると、status変数に終了コード、output変数に標準出力の値が設定される。
標準出力は、1行単位から確認することも可能。
テストの実行は bats <テストソース> で実行ができる。

#!/usr/bin/env /home/vagrant/bin/bats

setup() {

  . shell_func.sh

}

@test "add_success" {

  # Expected Define
  local exp_exit_code=0
  local exp_result=30

  # Test
  run add 10 20

  # Verify
  [ "${status}" -eq ${exp_exit_code} ]
  [ "${output}" == "${exp_result}" ]

}

@test "add_parameter_error" {

  # Expected Define
  local exp_exit_code=1
  local exp_err_msg="Argument Error!!"

  # Test
  run add 10

  # Verify
  [ "${status}" -eq ${exp_exit_code} ]
  [ "${output}" == "${exp_err_msg}" ]

}

Shellspec

セットアップ方法がいくつか公開されているが、Batsと同じようにユーザのホームディレクトリにセットアップする。
Githubリポジトリをクローンし、パスが通っているディレクトリにシンボリックリンクを作成する。

github.com

$ cd $HOME
$ git clone git clone https://github.com/shellspec/shellspec.git
$ ln -s $HOME/shellspec/shellspec $HOME/bin/shellspec

shellspec --initでプロジェクトを作成して、specフォルダにテストソースを作成する。

$ shellspec --init
$ cd spec

テストソースはIncludeにテスト対象のシェルスクリプトファイルを定義する。 Batsと同じにようにテスト対象の関数の終了コードはstatusに、標準出力はoutputに格納されている。 specフォルダ直下でshellspecコマンドを実行すれば*_spec.shのテストコードが実行される。

Describe 'shell func'
  Include ../shell/shell_func.sh
  It 'add func'
    When call add 10 20
    The status should equal 0
    The output should equal 30
  End
End

Describe 'shell func'
  Include ../shell/shell_func.sh
  It 'add error'
    When call add 10
    The status should equal 1
    The error should equal "Argument Error!!"
  End
End

shunit2

セットアップ方法は、一番シンプル。
Githubからリポジトリをクローンするだけでセットアップは完了。

$ git clone https://github.com/kward/shunit2.git

テストソースもJUnitのテストソースを書いたことがある人は、馴染みのあるテストソースになる。 確認用の関数が用意されているので、その関数を使用して確認をする。
BatsやShellspecのように終了コードが指定の変数に格納されないため、自分で終了コードを格納する必要がある。

#!/bin/bash

function test_add_success() {

  # Expected Define
  local exp_exit_code=0
  local exp_result=30

  # Test
  local act_result=$(add 10 20)
  local act_exit_code=$?

  # Verify
  assertEquals ${exp_exit_code} ${act_exit_code}
  assertEquals ${exp_result} ${act_result}

}

function test_add_param_error() {

  # Expected Define
  local exp_exit_code=1
  local exp_error_msg="Argument Error!!"

  # Test
  act_msg=$(add 10 2>&1)
  act_exit_code=$?

  # Verify
  assertEquals "Exit Code unmatch" ${exp_exit_code} ${act_exit_code}
  assertEquals "Error Msg unmatch" "${exp_error_msg}" "${act_msg}"


}

どれが使いやすい のか?

正直、どれも使いやすい。
学習コストを抑えて、即時適用したいのであれば、shUnit2が最もシンプルかと思う。
ただ、1~2日の学習コストが取れるのであれば、shUnit2よりもbatsがおすすめ。
標準出力されるメッセージを検査するとなると、shUnit2だとメッセージをまるまる比較しないといけないため、複数行になると期待値を作成するのが大変だが、Batsでは標準出力の結果を配列で保持してくれているので、1行ずつ確認することができる。
Shellspecは高機能なため、1時間ぐらいでサクッとテストが作成できる気がしない。
実際に、業務のツールのテストソースを作成してみたが、サクッと作れなかった。BDDのテストソースを作ったことがないので、作成の容量が掴めなかったのもあるかもしれない。
それに比べて、shUnit2は非常にシンプルなのでJUnitを作ったことがあれば、サクッとできるはず。
ただ、高機能なので学習コストを1~3日取れば、リターンは大きいと思う。 kcovが必要になるが、カバレッジも取れて、JUnit XML formatterでもテスト結果の出力ができることは魅了的だ。(BatsもJUnit XML formatterは対応している。)

結局のところ、どれがいいとははっきりと言えないが、どれもテストフレームワークとして十分な機能がある。
あとは学習コストやテスト作成のスケジュール、今後のメンテナンスを考えたときに何を使うべきかの判断になるだろう。

他にもテストフレームワークがあった

Bashのテストフレームワークを調べている人がいた。
bash_unitってのもあるらしい。

github.com