tl;dr

とある勉強会で, @pyama86 さんに「うちではシェルスクリプトのテストは shunit2 で書いていますよー」って教えて頂いたので, 早速導入してみた話をダイジェストで.

shUnit2

shUnit2 とは

Bourne Shell をベースとした, シェルスクリプトの為のユニットテストフレームワークです.

shunit2 - shUnit2 is a xUnit based unit test framework for Bourne based shell scripts.

github.com

README をざっと読んだ限りだと, xUnit と呼ばれるテストフレームワークと同じような使い方が出来るようなので, xUnit を少しでも触ったことがあれば, すんなりと利用することが出来そうです.

shUnit2 で Hello World

早速, Hello World してみたいと思います.

事前に, shunit2 を任意のディレクトリにダウンロードしておきます.

wget https://raw.githubusercontent.com/kward/shunit2/master/shunit2

今回はシンプルに以下のようなディレクトリ構成にしました.

$ tree sample
sample
├── sample_test.sh
└── shunit2

0 directories, 2 files

そして, 以下のようなスクリプトを書きました.

#!/usr/bin/env bash
# filename: sample_test.sh

# テストされる側の関数
function sample() {
  echo "Hello World"
}

# テスト関数
testSample() {
  output=`sample`
  assertEquals "${output}" "Hello World"
}

# shUnit2 は最後に読み込んであげる必要がある
. ./shunit2

assertEquals で, sample 関数の実行結果が Hello World と出力されることをテストしています.

実際にこのスクリプトを実行すると, 以下のように出力されてテストが通ります.

$ chmod +x sample_test.sh
$ ./sample_test.sh
testSample

Ran 1 test.

OK

LGTM.

shUnit2 諸々

詳細については…

shunit2 の詳細については README を見て頂くとして, 自分が実際に利用した Asserts 関数や Setup/Teardown 関数についてメモっておきます.

Assert

assertEquals

expectedactual の文字列が同一であることをテストします.

assertEquals [message] expected actual

以下のように書くと, 同一でなかった場合に指定したメッセージを出力させることが出来ます.

# テスト関数
testSample() {
  output=`sample`
  assertEquals "残念でした." "${output}" "Hello World"
}

実行すると, 以下のように出力されます.

$ ./sample_test.sh
testSample
ASSERT:残念でした. expected:<ello World> but was:<Hello World>

Ran 1 test.

FAILED (failures=1)

assertNotEquals
assertNotEquals [message] expected actual

他にも assertSameassertNull 等がありますが, 個人的に, これらの Assert だけで事足りた次第です.

Setup/Teardown

oneTimeSetup

全てのテストが実行される前に一度だけ呼び出される関数です.

# テスト関数
testSample1() {
  assertEquals "Hello World" "Hello World"
}

# 全てテストが実行される前に一度だけ呼び出される関数
oneTimeSetUp() {
  echo "oneTimeSetup が呼び出されました"
}

実行すると, 以下のように出力されます.

$ ./sample_test.sh
oneTimeSetup が呼び出されました
testSample1

Ran 1 test.

OK

oneTimeTearDown

全てのテストが完了した際に一度だけ呼び出される関数です.

# テスト関数
testSample1() {
  assertEquals "Hello World" "Hello World"
}

# 全てのテストが実行された後に一度だけ呼び出される関数
oneTimeTearDown() {
  echo "oneTimeTearDown が呼び出されました"
}
. ./shunit2

実行すると, 以下のように出力されます.

$ ./sample_test.sh
testSample1
oneTimeTearDown が呼び出されました

Ran 1 test.

OK

ちなみに, setUp と tearDown

setUp() や tearDown() はそれぞれのテスト前後に呼び出されます.

# テスト関数
testSample1() {
  assertEquals "Hello World" "Hello World"
}

# テスト関数
testSample2() {
  assertEquals "Hello shUnit2" "Hello shUnit2"
}

# テストが実行される前に毎回呼び出される関数
setUp() {
  echo "setUp が呼び出されました"
}

# テストが実行される後に毎回呼び出される関数
tearDown() {
  echo "tearDown が呼び出されました"
}

# shUnit2 は最後に読み込んであげる必要がある
. ./shunit2

実行すると, 以下のように出力されます.

$ ./sample_test.sh
setUp が呼び出されました
testSample1
tearDown が呼び出されました
setUp が呼び出されました
testSample2
tearDown が呼び出されました

Ran 2 tests.

OK

イイ感じです.

awscli を使ったシェルスクリプトでテストする

さて, 今回

awscli をラップするシェルスクリプトを作る機会がありましたので, このシェルスクリプトのテストを shUnit2 で書いてみました. その際に, AWS の API レスポンスを返してくれる moto という Python のライブラリと組み合わせることで, 生の AWS リソースに対する API リクエストを行うことなくテストを行うことが出来たので, moto_server はお薦めのツールだと思います.

moto - Moto is a library that allows your python tests to easily mock out the boto library

github.com

ちなみに, moto を少しこじらせてしまって, PyFukuoka #4 で LT させて頂きました.

speakerdeck.com

サンプル

シェルスクリプト

以下のような超シンプルなシェルスクリプト (関数) を用意しました.

#!/usr/bin/env bash -e
# filename: sample.sh

function get_objects() {
  #######################################
  # S3 オブジェクトを取得する
  # Globals:
  #   None
  # Arguments:
  #   None
  # Returns:
  #   オブジェクトの内容
  #######################################

  bucket=${1}
  key=${2}
  if [ "${_ENV}" == "test" -o "${_ENV}" == "debug" ];then
    obj=$(aws --endpoint=http://127.0.0.1:5000/ s3 cp s3://${bucket}/${key} -)
  else
    obj=$(aws s3 cp s3://${bucket}/${key} -)
  fi
  echo $obj
}

関数の引数で指定した S3 バケットとキーの内容を出力する関数です. 以下のように利用することを想定しています.

#!/usr/bin/env bash

... 省略 ...

get_objects ${your_bucket} ${your_key}

尚, シェルスクリプトを実行する際に _ENV=test 又は _ENV=debug を付与して実行した場合, moto で用意した API レスポンスを擬似的に返すサーバー (moto_server) 向けにリクエストを送信するように --endpoint=http://127.0.0.1:5000/ を指定しています.

テスト

テストスクリプトは以下のように書きました. oneTimeSetUp 関数で moto_server を起動して, テスト用のオブジェクトを moto_server 上に作成した S3 バケットに登録しています. また, oneTimeTearDown 関数で moto_server を停止させる処理を実行しています. 尚, moto_server を利用する為には, 事前に moto_server をインストールしておく必要があります. 詳しくは moto の README を御確認下さい.

#!/usr/bin/env bash
# filename: sample_test.sh

. ./sample.sh

oneTimeSetUp() {
  #######################################
  # moto_server の起動, S3 バケットを作成, S3 バケットにオブジェクトを登録
  #######################################

  moto_server s3 > moto_server.log 2>&1 &
  echo "foo bar baz" > data1.txt
  aws --endpoint=http://127.0.0.1:5000 s3api create-bucket --bucket=sample-bucket > /dev/null
  aws --endpoint=http://127.0.0.1:5000 s3api put-object --bucket=sample-bucket --key=data1 --body=data1.txt > /dev/null
  rm -f data1.txt
}

oneTimeTearDown() {
  #######################################
  # moto_server の停止
  #######################################

  pid=$(ps aux | grep [m]oto_server | awk '{print $2}')
  kill ${pid}
}


testGetObjects() {
  #######################################
  # get_objects のテスト
  #######################################

  assertEquals "$(get_objects 'sample-bucket' 'data1')" "foo bar baz"
}

. ./shunit2

テストを実行すると, 以下のように出力されてテストは成功します.

$ _ENV=test ./sample_test.sh
testGetObjects

Ran 1 test.

OK

一応, moto_server.log を見てみます.

$ cat moto_server.log
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [30/Apr/2018 22:48:15] "PUT /sample-bucket HTTP/1.1" 200 -
127.0.0.1 - - [30/Apr/2018 22:48:17] "PUT /sample-bucket/data1 HTTP/1.1" 200 -
127.0.0.1 - - [30/Apr/2018 22:48:20] "HEAD /sample-bucket/data1 HTTP/1.1" 200 -
127.0.0.1 - - [30/Apr/2018 22:48:20] "GET /sample-bucket/data1 HTTP/1.1" 200 -

テストが実行された際のアクセスログが記録されています.

イイ感じです.

以上

超簡単に shUnit2 について紹介させて頂きました. 今後はシェルスクリプトを作った際には, 出来るだけ shUnit2 を使ったテストを書けるようになりたいと思います. また, moto (moto_server) と組み合わせることで, 生の AWS リソースを触ることなく awscli を使ったシェルスクリプトのテストについても shUnit2 で書くことが出来るので, 無理の無い範囲でテストを書いていければと考えています.

有難うございました.

元記事はこちら

shUnit2 で awscli を使ったシェルスクリプトのユニットテストを書くまでの諸々