- tl;dr
- shUnit2
— shUnit2 とは
— shUnit2 で Hello World
— shUnit2 諸々
—– 詳細については…
—– Assert
—– assertEquals
—– assertNotEquals
—– Setup/Teardown
—– oneTimeSetup
—– oneTimeTearDown
—– ちなみに, setUp と tearDown - awscli を使ったシェルスクリプトでテストする
— さて, 今回
— サンプル
—– シェルスクリプト
—– テスト - 以上
tl;dr
とある勉強会で, @pyama86 さんに「うちではシェルスクリプトのテストは shunit2 で書いていますよー」って教えて頂いたので, 早速導入してみた話をダイジェストで.
shUnit2
shUnit2 とは
Bourne Shell をベースとした, シェルスクリプトの為のユニットテストフレームワークです.
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
expected
と actual
の文字列が同一であることをテストします.
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
他にも assertSame
や assertNull
等がありますが, 個人的に, これらの 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 はお薦めのツールだと思います.
github.com
ちなみに, moto を少しこじらせてしまって, PyFukuoka #4 で LT させて頂きました.
サンプル
シェルスクリプト
以下のような超シンプルなシェルスクリプト (関数) を用意しました.
#!/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 で書くことが出来るので, 無理の無い範囲でテストを書いていければと考えています.
有難うございました.