シェルスクリプトでユニットテストを書いてみよう #TddAdventJp

この記事はTDD Advent Calendar jp: 2012の14日目の記事です。前日はposaunehmさんのJUnit実践入門 MSTest用パッチ #TddAdventJpでした。

プログラマであれば、多かれ少なかれシェルスクリプトを書いた経験はあると思います。でも、シェルスクリプトでユニットテストを書いたことのある人は意外に少ないのではないでしょうか。

シェルスクリプトでユニットテストを書くには?

shunit2 を使います。ダウンロードして解凍してexamplesの下にあるシェルを実行すればサンプルが動きます。

$ sh ./equality_test.sh
testEquality

Ran 1 test.

OK  

使い方

test で始まるファンクションがテスト対象になります。また、テストスクリプトの最後でshunit2を読み込む必要があります。

#! /bin/sh

# test で始まるファンクションがテスト対象になる
testSample() {  
  # ファイルを吐けるか確認
  echo "test" > ./test.txt
  assertEquals $? 0
}

# test で始まらないファンクションはテスト対象にならない
sampleTest() {  
  echo "test2" > ./test2.txt
  assertEquals $? 1
}

# shunit2 を読み込みます。これは最後に書かないといけない
. ./shunit2-2.1.6/src/shunit2

実行結果

$ sh ./sample_test.sh
testSample

Ran 1 test.

OK  

setUp や tearDown なども準備されています。

#!/bin/sh

# テスト前処理
oneTimeSetUp() {  
  # テストに使うディレクトリを作る
  mkdir -p ./test/
}

# 各テストの前処理
setUp() {  
  # テストに使うファイルを作る
  echo "test" > ./test/test.txt
}

# 各テストの後処理
tearDown() {  
  # テストに使ったファイルを消す
  rm ./test/*
}

# テスト後処理
oneTimeTearDown() {  
  # 最後にディレクトリ毎消しておく
  rm -rf ./test
}

testSample1() {  
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 1

  echo "abc" >> ./test/test.txt
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 2
}

testSample2() {  
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 1

  echo "abc" > ./test/test.txt
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 1
}

. ./shunit2-2.1.6/src/shunit2

実行結果

$ ls
sample_test.sh  sample_test2.sh  shunit2-2.1.6  
$ sh ./sample_test2.sh
testSample1  
testSample2

Ran 2 tests.

OK  
$ ls
sample_test.sh  sample_test2.sh  shunit2-2.1.6  

assertEquals 以外にも、assertTrue とか assertNotNull とか、そこそこ揃っています。

#! /bin/sh

testSample() {  
  assertEquals $? 0
  assertTrue "true"
  assertTrue 0
  assertFalse "false"
  assertFalse 1
  assertNotNull "aaa"
}

. ./shunit2-2.1.6/src/shunit2

実行結果

$ sh ./sample_test3.sh
testSample

Ran 1 test.

OK  

shunit2の欠点を克服する

実際にshunit2を試してみた方ならすぐに気づいたと思いますが、shunit2にはTDDerにとって非常に重大な欠点があります。それは、テストに失敗したとき赤くならないこと。これは気分的にも盛り上がりませんし、シェルであれば色つきで標準出力することも簡単にできるので、shunit2 に少し手を入れて改善してしまいましょう。

shunit2-2.1.6/src/shunit2 の 808行目から始まる shunitgenerateReport() でテスト結果を表示しているので、修正します。

# Generates the user friendly report with appropriate OK/FAILED message.
#
# Args:
#   None
# Output:
#   string: the report of successful and failed tests, as well as totals.
_shunit_generateReport()  
{
  _shunit_ok_=${SHUNIT_TRUE}

  # if no exit code was provided one, determine an appropriate one
  [ ${__shunit_testsFailed} -gt 0 \
      -o ${__shunit_testSuccess} -eq ${SHUNIT_FALSE} ] \
          && _shunit_ok_=${SHUNIT_FALSE}

  echo
  if [ ${__shunit_testsTotal} -eq 1 ]; then
    echo "Ran ${__shunit_testsTotal} test."
  else
    echo "Ran ${__shunit_testsTotal} tests."
  fi

  _shunit_failures_=''
  _shunit_skipped_=''
  [ ${__shunit_assertsFailed} -gt 0 ] \
      && _shunit_failures_="failures=${__shunit_assertsFailed}"
  [ ${__shunit_assertsSkipped} -gt 0 ] \
      && _shunit_skipped_="skipped=${__shunit_assertsSkipped}"

  if [ ${_shunit_ok_} -eq ${SHUNIT_TRUE} ]; then
## OK のときは緑色にする
#    _shunit_msg_='OK'
    _shunit_msg_='\e[32mOK\e[m'
    [ -n "${_shunit_skipped_}" ] \
        && _shunit_msg_="${_shunit_msg_} (${_shunit_skipped_})"
  else
## NG のときは赤色にする
#    _shunit_msg_="FAILED (${_shunit_failures_}"
#    [ -n "${_shunit_skipped_}" ] \
#        && _shunit_msg_="${_shunit_msg_},${_shunit_skipped_}"
#    _shunit_msg_="${_shunit_msg_})"

    _shunit_msg_="\e[31mFAILED (${_shunit_failures_}"
    [ -n "${_shunit_skipped_}" ] \
        && _shunit_msg_="${_shunit_msg_},${_shunit_skipped_}"
    _shunit_msg_="${_shunit_msg_})\e[m"
  fi

  echo
## -e オプションをつけてエスケープを有効にする
#  echo ${_shunit_msg_}
  echo -e ${_shunit_msg_}
  __shunit_reportGenerated=${SHUNIT_TRUE}

  unset _shunit_failures_ _shunit_msg_ _shunit_ok_ _shunit_skipped_
}

これでOKやFAILEDがちゃんと緑や赤で表示されるようになりました。

さいごに

実際にTDDでシェルスクリプトを書いてみたら、シェルのことを何も知らなったことに気づきました。TDDをやるとなんとなくでコードが書けなくなるので、理解の浅い言語でTDDをすると非常に勉強になります。TDDでシェルスクリプトを書いたことがない人は、ぜひ一度書いてみてはどうでしょうか。