shUnit2 †
xUnit系テストツールのシェル版である。
インストール †
特別な操作は不要。
ソースをダウンロードして、任意の場所に設置するだけである。
非常に簡単である。
テスト †
特別な設定は不要であり、典型的な実行方法はテストプログラムの中でshunit2プログラムをインクルードするだけである。
例 sample.sh
#! /bin/sh testEquality() { assertEquals 1 1 } # shunit2テストをインクルード、テストが実行される . /path/to/shunit2
テストプログラム †
- テストファイルは、suffixが
_test.sh
である。 - テスト対象の関数は、
test
を接頭辞に持つものである。
# テスト対象 testVarShouldNotbeEmpty() { assertNotNull '$var is not empty' "$msg" } # テスト対象でない hoge() { assertNotNull '$var is not empty' "$msg" }
- テストは、ファイル内の上から定義されている順に実行される。
補足
- テスト関数は
eval
で評価される。 egrep
で以下の条件にマッチする関数名がテスト対象である。^\s*((function test[A-Za-z0-9_-]*)|(test[A-Za-z0-9_-]* *\(\)))
テストの実行 †
shunit2から実行 †
テストは、テストプログラム内からshunit2をロードする、または、shunit2にテスト対象ファイル名を渡すことで実行される。
$ shunit2 <a test file> [<func> [<func>...]]
sample_test.sh
#! /bin/sh testEquality() { assertEquals 1 1 } testTrue() { assertTrue "[ 1 -eq 1 ]" }
実行
ファイルのテスト関数をすべて実行する。
$ shunit2 sample_test.sh
testEquality
testTrue
Ran 2 tests.
OK
ファイルの特定のテスト関数を実行する。
$ shunit2 sample_test.sh testTrue
testTrue
Ran 1 test.
OK
shunit2をインクルードして実行 †
hoge_test.sh
# hoge_test.sh testFunc() { : } . /path/to/shunit2 # この場合、$0がテスト対象ファイルと見なされる(つまり、hoge_test.sh)
実行
$ bash hoge_test.sh
テストスイートの実行 †
test_runner †
テスト実行のためのヘルパーである。
suffixが、_test.sh
であるテストファイル見つけてまとめて実行してくれる(テストスイートの実行である)。
特定のシェル環境で実行したり、指定がしなければデフォルトで複数のシェル環境でテストを実行してくれる。
$ ./test_runner -h usage: test_runner [-e key=val ...] [-s shell(s)] [-t test(s)]
デフォルトのシェル環境は以下のとおり。
/bin/sh ash /bin/bash /bin/dash /bin/ksh /bin/pdksh /bin/zsh
サンプル test_runnerの実行例
shunit2の配下にあるテストでない場合は、shunit2のlib
ディレクトリの場所を指定すると流れる。
テスト対象は、テスト実行ディレクトリ($PWD
)にある_test.sh
のファイルである。
$ tree ../shunit2 ../shunit2 ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── doc │ ├── CHANGES-2.1.md │ ├── RELEASE_NOTES-2.1.0.txt │ ├── RELEASE_NOTES-2.1.1.txt │ ├── RELEASE_NOTES-2.1.2.txt │ ├── RELEASE_NOTES-2.1.3.txt │ ├── RELEASE_NOTES-2.1.4.txt │ ├── RELEASE_NOTES-2.1.5.txt │ ├── RELEASE_NOTES-2.1.6.txt │ ├── RELEASE_NOTES-2.1.7.md │ ├── TODO.txt │ ├── contributors.md │ └── design_doc.txt ├── examples │ ├── equality_test.sh │ ├── lineno_test.sh │ ├── math.inc │ ├── math_test.sh │ ├── mkdir_test.sh │ ├── mock_file.sh │ ├── mock_file_test.sh │ ├── party_test.sh │ └── suite_test.sh ├── lib │ ├── shflags │ └── versions ├── shunit2 ├── shunit2_args_test.sh ├── shunit2_asserts_test.sh ├── shunit2_failures_test.sh ├── shunit2_macros_test.sh ├── shunit2_misc_test.sh ├── shunit2_standalone_test.sh ├── shunit2_test_helpers └── test_runner 3 directories, 35 files $ tree . └── hoge_test.sh 0 directories, 1 file $ LIB_DIR=../shunit2/lib ../shunit2/test_runner #------------------------------------------------------------------------------ # System data. # $ uname -mprsv Linux 3.10.0-1062.9.1.el7.x86_64 #1 SMP Fri Dec 6 15:49:49 UTC 2019 x86_64 x86_64 OS Name: Linux OS Version: CentOS Linux 7 (Core) ### Test run info. shells: /bin/sh ash /bin/bash /bin/dash /bin/ksh /bin/pdksh /bin/zsh tests: hoge_test.sh #------------------------------------------------------------------------------ # Running the test suite with /bin/sh. # shell name: sh shell version: GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu) --- Executing the 'hoge' test suite. --- testEquality Ran 1 test. OK #------------------------------------------------------------------------------ # Running the test suite with ash. # runner:WARN unable to run tests with the ash shell #------------------------------------------------------------------------------ # Running the test suite with /bin/bash. # shell name: bash shell version: GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu) --- Executing the 'hoge' test suite. --- testEquality Ran 1 test. OK #------------------------------------------------------------------------------ # Running the test suite with /bin/dash. # runner:WARN unable to run tests with the dash shell #------------------------------------------------------------------------------ # Running the test suite with /bin/ksh. # runner:WARN unable to run tests with the ksh shell #------------------------------------------------------------------------------ # Running the test suite with /bin/pdksh. # runner:WARN unable to run tests with the pdksh shell #------------------------------------------------------------------------------ # Running the test suite with /bin/zsh. # runner:WARN unable to run tests with the zsh shell
実験テストプログラムがshunit2と別のディレクトリにある場合
test_runner
のラッパーを作成して実行してみる(run.sh
)。
ディレクトリ構成
ディレクトリ構成は以下のとおり。
テストは、shunit2とは別のディレクトリにあるとする。
/home/guest/shunit2 |- shunit2 |- lib |- test_runner ... /home/guest/mytests |- my_test.sh |- run.sh
/home/guest/mytests/run.sh
test_runnerを実行するためだけのラッパー。
#!/usr/bin/env bash export SHUNIT2_ROOT=${SHUNIT2_ROOT:-/home/guest/shunit2} export LIB_DIR="$SHUNIT2_ROOT/lib" export SHUNIT_INC="$SHUNIT2_ROOT/shunit2" "$SHUNIT2_ROOT"/test_runner "$@"
/home/guest/mytests/my_test.sh
テストプログラムは、shunit2を環境変数で変えられるように作成しておく。
#! /bin/sh . "$SHUNIT2_ROOT"/shunit2_test_helpers testEquality() { assertEquals 1 1 assertEquals "$HOGE" "foo" } . "${TH_SHUNIT}"
以下では、シェルは/bin/sh
、テスト対象はmy_test.sh
、環境変数としてHOGE=foo
を定義して実行している。
$ cd /home/guest/mytests $ bash run.sh -s /bin/sh -t my_test.sh -e HOGE=foo #------------------------------------------------------------------------------ # System data. # $ uname -mprsv Linux 3.10.0-1062.9.1.el7.x86_64 #1 SMP Fri Dec 6 15:49:49 UTC 2019 x86_64 x86_64 OS Name: Linux OS Version: CentOS Linux 7 (Core) ### Test run info. shells: /bin/sh tests: my_test.sh HOGE=foo #------------------------------------------------------------------------------ # Running the test suite with /bin/sh. # shell name: sh shell version: GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu) --- Executing the 'my' test suite. --- testEquality Ran 1 test. OK
suite(独自にテスト関数を指定して実行) †
suite
を用いるとsuite_addTest
で任意の関数をテスト対象に指定することができる。
しかし、ソースコメントを見る限り2.1.0の時点でdeprecated
となっている。
例 suiteで独自のテスト関数を実行する
suite_test.sh
#! /bin/sh my_test1() { assertEquals 1 1 } my_test2() { assertTrue "[ 1 -eq 1 ]" } suite() { suite_addTest "my_test1" suite_addTest "my_test2" } . ../shunit2/shunit2
実行。
$ bash suite_test.sh my_test1 my_test2 Ran 2 tests. OK
注意
suite_addTest
が設定されると、デフォルトの "test" を接頭辞にもつ関数の自動的なテストは行われないことに注意。
これは、テスト対象が存在しない場合にのみtest
を接頭辞にもつテスト関数を探索するようになっているためである。
参考
特徴・機能 †
詳細はドキュメントを参照のこと。
動作で留意しておくべき点のみピックアップする。
oneTimeSetUp、oneTimeTearDown †
- テストの失敗にかかわらず
oneTimeTearDown
は実行される。 - テスト実行前、実行後に一度のみ実行される。
- oneTimeSetUp関数がエラーの場合、テストは停止する。
例 oneTimeSetUpで失敗した場合
oneTimeSetUp() { echo "oneTimeSetUp" return 1 } oneTimeTearDown() { echo "oneTimeTearDown" } setUp() { echo "setUp" } tearDown() { echo "tearDown" } testMy() { echo "testMy" assertEquals 1 1 } testMy2() { echo "testMy2" assertEquals 1 1 } . ../shunit2/shunit2
実行結果は以下のとおり。
$ bash study_test.sh oneTimeSetUp shunit2:FATAL oneTimeSetUp() returned non-zero return code. tearDown oneTimeTearDown ASSERT:unknown failure encountered running a test Ran 0 tests. FAILED (failures=1)
例 oneTimeTearDown で失敗した場合
当然ながらテストは実行された後のcleanupで失敗となる。
oneTimeSetUp() { echo "=> oneTimeSetUp" } oneTimeTearDown() { echo "=> oneTimeTearDown" return 1 } setUp() { echo "=> setUp" } tearDown() { echo "=> tearDown" } testMy() { echo "=> testMy" assertEquals 1 1 } testMy2() { echo "=> testMy2" assertEquals 1 1 } . ../shunit2/shunit2
実行結果は以下のとおり。
$ bash study4_test.sh => oneTimeSetUp => setUp testMy => testMy => tearDown => setUp testMy2 => testMy2 => tearDown => oneTimeTearDown shunit2:FATAL oneTimeTearDown() returned non-zero return code. => tearDown => oneTimeTearDown shunit2:WARN oneTimeTearDown() returned non-zero return code. ASSERT:unknown failure encountered running a test Ran 2 tests. FAILED (failures=1)
setUp、tearDown †
- テストごとに、テスト実行前、実行後に一度のみ実行される。
- テストの失敗にかかわらず
tearDown
は実行される。 setUp
、tearDown
が失敗した場合は、テストは終了する。
tearDown
はテスト終了時に再度呼ばれる。その後、oneTimeTearDown
が呼ばれる。
Ran 2 testsのようにレポートされるが、testXXX
は実行前であることに注意。
例 setUp関数がエラーの場合
oneTimeSetUp() { echo "oneTimeSetUp" } oneTimeTearDown() { echo "oneTimeTearDown" } setUp() { echo "setUp" return 1 } tearDown() { echo "tearDown" } testMy() { echo "testMy" assertEquals 1 1 } testMy2() { echo "testMy2" assertEquals 1 1 } . ../shunit2/shunit2
実行結果は以下のとおり。
$ bash study2_test.sh oneTimeSetUp setUp shunit2:FATAL setup() returned non-zero return code. tearDown oneTimeTearDown ASSERT:unknown failure encountered running a test Ran 2 tests. FAILED (failures=1)
例 tearDown関数がエラーの場合
なお、以下ではtestMyは失敗するようにしている。
oneTimeSetUp() { echo "oneTimeSetUp" } oneTimeTearDown() { echo "oneTimeTearDown" } setUp() { echo "setUp" } tearDown() { echo "tearDown" return 1 } testMy() { echo "Run testMy" assertEquals 1 2 } testMy2() { echo "Run testMy2" assertEquals 1 1 } . ../shunit2/shunit2
実行結果は以下のとおり。
$ bash study3_test.sh oneTimeSetUp setUp testMy Run testMy ASSERT:expected:<1> but was:<2> shunit2:ERROR testMy() returned non-zero return code. tearDown shunit2:FATAL tearDown() returned non-zero return code. tearDown shunit2:WARN tearDown() returned non-zero return code. oneTimeTearDown ASSERT:unknown failure encountered running a test Ran 2 tests. FAILED (failures=3)
Skipping †
assertのfailの実行と記録を行わない。
startSkippingとendSkippingでスキップ対象範囲を囲む。
メモ
これらの関数は内部的にスキップフラグをON/OFFにしているだけあり、assertやfail系関数の実行時にこのフラグが参照され、assertやfailを実行するか否かを判断している。
例 スキップの例
testSkip() { # startSkipping echo "This message will be printed" assertEquals 1 1 # endSkipping assertEquals 1 1 } . ../shunit2/shunit2
実行結果は以下のとおり。
$ bash skip_test.sh testSkip This message will be printed Ran 1 test. OK
Skipを有効にすると以下のとおり。
$ bash skip_test.sh testSkip This message will be printed Ran 1 test. OK (skipped=1)
レポート †
レポートで出力される内容は、以下のとおりである。
数値が何を意味しているのかは少し注意が必要(failures)である。
表示 | 意味 |
---|---|
Ran n tests | nは、テスト対象となったテスト関数の数。テスト開始時に決まる。 |
skipped= | assertやfailがスキップされた数 |
failures= | 失敗した実行数(assert、fail、テスト) |
Ran n tests
のnは、テスト対象関数として定義された時点でのテスト数である。
実行されていなくとも、setUp
関数で失敗してもRan n tests
と表示されるのはそのためである。
実験 テスト実行前にテスト終了させる場合のレポート
通常はこのような事をしないので、あくまで実験である。
my_test1() { assertEquals 1 1 } my_test2() { assertTrue "[ 1 -eq 1 ]" } suite() { suite_addTest "my_test1" suite_addTest "my_test2" exit # ここで停止してもRan 2 testsとなるはず } . ../shunit2/shunit2
実行すると以下のように Ran 2 tests
となる。
bash suite_test.sh ASSERT:unknown failure encountered running a test Ran 2 tests. FAILED (failures=1)
メモ テストの実行プロセス
テストの実行プロセスは大まかに以下のとおり。
- テスト対象ファイルの認識
- oneTimeSetUpの実行
- suiteがあればsuiteを実行。もしくは、<file> 関数名...が指定されて実行された場合は、shunit2が面倒を見てくれる。
(suiteは実行されない) - 登録済みのテスト対象関数がなければ、shunit2がtestの接頭辞のテスト対象関数を抽出
- テストスイートの実行
- oneTimeTearDownの実行
- テストレポートの出力
補足 詳細なテストレポート
assertの数など少し詳細な情報が欲しいかもしれない。
内部的には統計情報を持っているが、レポート生成関数では参照されていないようであった。
以下のようにすれば内部の情報も見ることができた。
(他に正当なやり方があるかもしれないが調べる限り見つからなかった)
test_test1() { startSkipping fail "This is not counted" assertEquals 1 1 endSkipping } test_test2() { assertTrue "[ 1 -eq 1 ]" fail "hoge" assertTrue "[ 1 -eq 2 ]" } _DETAIL_REPORTED=0 oneTimeTearDown() { if [ $_DETAIL_REPORTED -ne 1 ]; then echo "=== Detail Report ===" echo "testsTotal=${__shunit_testsTotal}" # Run n tests の n echo "testsPassed=${__shunit_testsPassed}" echo "testsFailed=${__shunit_testsFailed}" echo "assertsTotal=${__shunit_assertsTotal}" echo "assertsPassed=${__shunit_assertsPassed}" echo "assertsFailed=${__shunit_assertsFailed}" # failures=の値 echo "assertsSkipped=${__shunit_assertsSkipped}" # skipped=の値 echo "======" _DETAIL_REPORTED=1 fi } . ../shunit2/shunit2
実行結果は以下のとおり。
$ bash suite2_test.sh test_test1 test_test2 ASSERT:hoge ASSERT: shunit2:ERROR test_test2() returned non-zero return code. === Detail Report === testsTotal=2 testsPassed=1 testsFailed=1 assertsTotal=6 assertsPassed=1 assertsFailed=3 assertsSkipped=2 ====== Ran 2 tests. FAILED (failures=3,skipped=2)
各表示の意味は以下のとおり。
表示 | 意味 |
---|---|
testsTotal | テスト数、suite_addTestされたテスト関数の数 |
testsPassed | テストにパスした数、テスト関数単位 |
testsFailed | テストに失敗した数、テスト関数単位 |
assertsTotal | skipを含む、assert、failの数 |
assertsPassed | assertにパスした数 |
assertsFailed | assert失敗、fail、テストに失敗した数 |
assertsSkipped | スキップされたassert、failの数 |
assertsFailed
の数が見た目とずれている気がするかもしれない。
これは、assertやfailの失敗以外もfailedとしてカウントされているためである。
https://github.com/kward/shunit2/issues/117
test_test2() { assertTrue "[ 1 -eq 1 ]" fail "hoge" assertTrue "[ 1 -eq 2 ]" # 上記のassertで失敗するため、テスト関数もエラー終了となり、assertFailedもインクリメントされる。 }
以下のようにすれば期待する結果となる。
test_test2() { assertTrue "[ 1 -eq 1 ]" fail "hoge" assertTrue "[ 1 -eq 2 ]" : # return 0 でもOK }
実験 suiteの途中で不意に終了した例
もう1つassertFailedに関して実験してみる。
assertFailedがfailuresに含まれる例である。
#! /bin/sh my_test1() { assertEquals 1 1 } suite() { suite_addTest "my_test1" exit } _DETAIL_REPORTED=0 oneTimeTearDown() { if [ $_DETAIL_REPORTED -ne 1 ]; then echo "=== Detail Report ===" echo "testsTotal=${__shunit_testsTotal}" echo "testsPassed=${__shunit_testsPassed}" echo "testsFailed=${__shunit_testsFailed}" echo "assertsTotal=${__shunit_assertsTotal}" echo "assertsPassed=${__shunit_assertsPassed}" echo "assertsFailed=${__shunit_assertsFailed}" echo "assertsSkipped=${__shunit_assertsSkipped}" echo "======" _DETAIL_REPORTED=1 fi } . ../shunit2/shunit2
残念ながらこの結果からは、__shunit_assertsFailed
が1
にはならない。
oneTimeTearDown
が実行される段階では、__shunit_assertsFailed
がインクリメントされていないからである。
$ bash suite4_test.sh === Detail Report === testsTotal=1 testsPassed=0 testsFailed=0 assertsTotal=0 assertsPassed=0 assertsFailed=0 assertsSkipped=0 ====== ASSERT:unknown failure encountered running a test Ran 1 test. FAILED (failures=1)
実行コンテキスト †
shUnit2のテスト実行コンテキストは、テストファイル内の各テストで共通である。
つまり、各テストはサンドボックス内で実行されていないため、他のテストに影響しうるということである。
例を見てみよう。
極端な例ではあるが、イメージが掴めると思う。
実験 shunit2のテスト実行コンテキスト
~/bin/ls
$ cat ~/bin/ls #!/usr/bin/env bash echo "myls"
sandbox_test.sh
test_no1() { export PATH="~/bin":"$PATH" result=$(ls) assertEquals "$result" "myls" } test_no2() { touch "test.txt" result=$(ls "test.txt") assertEquals "$result" "test.txt" } source ../shunit2/shunit2
実行すると以下のようになる。
$ bash sandbox_test.sh test_no1 test_no2 ASSERT:expected:<myls> but was:<test.txt> shunit2:ERROR test_no2() returned non-zero return code. Ran 2 tests. FAILED (failures=2)
2番目のテストtest_no2
で失敗していることが分かる。
これは、test_no1
で実行したexport PATH
がtest_no2
でも生きているということである。
テスト実行のコンテキストで、eval
を使ってテスト関数を実行しているためである。
あるテストで行なった他に影響し得る変更は、テストの最後でリセットするようにすればよい。
例えば、Bashで書かれたテストフレームワークであるbatsでは、各テストがサンドボックス内(サブシェル)で実行されるため、あるテストで定義した変数や環境変数等の定義は、他のテストには影響しない。