はじめに(Summary)
Baut(Bash Unit test Tool)は、Bashで書かれたユニットテストツールです。シェルスクリプトだけでなくUnix/Linuxプログラムが期待通りの動作をするかを検証するのに有用です。
Bautが実行するテストプログラムは、単なるBashスクリプトです。特別なエディターやエディターモードも必要ありません。使い慣れたいつもの環境で簡単にテストを記述することができます。
Bautのテストプログラムは、以下のようになります。他のテストフレームワークと類似しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#: @BeforeEach function setup() { echo "==> $(self)" export PATH=/usr/local/bin:"$PATH" touch flagfile } #: @Test(The usage should be displayed when command line options are invalid) function parse_cli_options() { echo "==> $(self)" run ./myshell.sh [[ "$result" =~ usage: ]] } #: @Test #: @Ignore function this_test_is_ignored() { echo "This test is ignored" } #: @AfterEach function teardown() { echo "==> $(self)" /bin/rm flagfile } |
#:
で始まる行は、続く記号に何らかの意味を持つ記述が来ることを示すメタコメント行です。メタコメント行には、@
で始まる識別コードを記述します。@BeforeEach
、@AfterEach
はそれぞれあるテスト開始前と終了時に呼ばれる関数です。1テストごとに呼ばれます。@Test(...)
は、関数がテスト対象であることを示します。変わりに「test_*
」にマッチする関数名であれば、@Test
がなくてもテスト対象とみなします。function
キーワードはなくても問題ありません。@Ignore
が指定されたテストは無視されます。テスト対象に含まれないので、テストとしてカウントされません。
コマンドの実行が正しいか否かを確認するコードは通常のシェルの条件式です。set -e
と同様にエラーを検出します。しかし、BautではERRトラップを使ってエラーを捕捉しているのでテスト自体はすぐに終了となりません。$result
は、run
コマンドで実行したコマンドの標準出力及び標準エラー出力の結果が格納されています。
上記を実行した結果は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
1 file, 1 test [1] /Users/guest/test/test_sample.sh x The usage should be displayed when command line options are invalid ==> setup ==> parse_cli_options # Error(127) detected at the following: # 6 } # 7 # 8 #: @Test(The usage should be displayed when command line options are invalid) # 9 function parse_cli_options() { # 10 echo "==> $(self)" #> 11 run ./myshell.sh # 12 [[ "$result" =~ usage: ]] # 13 } # 14 ==> teardown 1 test, 0 ok, 1 failed, 0 skipped ? 1 file, 1 test, 0 ok, 1 failed, 0 skipped Time: 0 hour, 0 minute, 0 second |
分かりやすいように各関数の始めにecho
で関数名を出力しました。上記では、テストファイルの11行目のrunコマンドでエラーになったことを示しています。テスト明細行には、1つのtestを実行し1つが失敗したことを示しています。最終の2行は、テスト全体をとしての明細です。Timeはテスト実行にかかった時間です。
インストール(Install)
インストールは特別な処理は必要ありません。githubからcloneするだけですぐに使用可能です。BautはBash4.2以上が必要です。
1 2 |
$ git clone https://github.com/moritoru81/baut.git $ source install.sh |
install.sh
は、git clone
したbautの実行ディレクトリにパスを設定します。実行しなくても問題ありません。
ダウンロード後、以下のコマンドを実行するとhelpが表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ ./bin/baut -h Usage: baut [-v] [-h] [--d[0-4]] [run|<command>] [<args>] OPTIONS -v, --version Show version. -h, --help Show usage. --d[0-4] Set log level to TRACE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4) COMMANDS compile Compile a test script file. run Run tests. test Run tests in a file and print its result. Ordinally this command is called in 'run' command. Show more available information about a specific command. 'baut <command> [-h|--help]' |
使用方法(Usage)
runコマンド
テスト実行は、テストのためのシェルスクリプトを書きrun
コマンドで実行するだけです。Bautは、指定されたテストファイルを読み、テスト実行のための準備を行なった後、テストファイルの各テストを実行していきます。
1 2 3 4 5 6 7 8 9 10 |
$ tree test/ test/ └── test_sample.sh $ cat test/test_sample.sh #: @Test(The usage should be displayed when command line options are invalid) function parse_cli_options() { run echo "usage: baut" [[ "$result" =~ ^usage:[[:space:]]baut$ ]] } |
なお、テストファイルは以下の規約に従う必要があります。
test_*.sh
にマッチする。
テストの準備ができたらrunコマンドで実行します。
1 2 3 4 5 6 7 8 |
$ baut run test/test_sample.sh 1 file, 1 test [1] /Users/guest/workspace/baut/test/test_sample.sh o The usage should be displayed when command line options are invalid 1 test, 1 ok, 0 failed, 0 skipped ? 1 file, 1 test, 1 ok, 0 failed, 0 skipped Time: 0 hour, 0 minute, 0 second |
1つのテストファイル、1つのテストを実行し成功しました。
オプション
–stop-on-error
テストで失敗した場合、残りのテストを実行せずテストを終了します。
–dry-run
テストで実行される関数を表示します。テストは実行されません。
–no-color
出力カラーをoffにします。
–no-checksum
コンパイル時と実行時におけるチェックサムの比較検証をスキップします。
–format [oneline|default|tap|cat]
出力フォーマットを指定します。
- onelineは、テスト名と結果を1行で出力します。テスト内での出力は破棄されます。
- defaultは、デフォルト動作です。テスト内の出力もそのまま表示します。
- tapは、tapプロトコルに沿った出力形式です。
- catは、テスト実行時の加工のない出力です。主にデバッグ時にしか使用しません。
compileコマンド
通常、runコマンドから呼ばれるため明示的に呼ぶ機会はありません。Bautがテスト実行時に読み込むスクリプトを出力します。
testコマンド
通常、runコマンドから呼ばれるため明示的に呼ぶ機会はありません。compileされたファイルを引数にとりテストを実行します。
テストを書く(Writing Scripts)
テストファイルは、単なるBashスクリプトであることは先ほど説明の通りです。ここでは、テストの記述ルールについて説明します。
テストファイル(Test File)
テストファイル名は、test_
で始まり、拡張子は.sh
である必要があります。拡張子が.sh
であるのは、通常のエディタにおいて特別な設定が不要なことを意味します。ファイルを開けば、シェルモードでインデントや補完などを使ってすぐにテストを記述することができます。
ファイル階層(Directory Structure)
テストファイルは、任意のディレクトリに設置可能です。ディレクトリの階層が深くても問題ありません。テストの種類別にディレクトリを分けてテストファイルを格納することができます。
1 2 3 4 5 6 7 8 9 |
$ tree . . ├── command ├── database ├── options │ └── test_options.sh └── test_sample.sh 3 directories, 2 files |
run
コマンドで-r
オプションをつけることで、再帰的にテストファイルを探索します。
1 2 3 4 5 6 7 8 9 10 11 |
$ baut run -r . 2 files, 2 tests [1] /Users/guest/workspace/baut/test/options/test_options.sh o test_help_options 1 test, 1 ok, 0 failed, 0 skipped [2] /Users/guest/workspace/baut/test/test_sample.sh o The usage should be displayed when command line options are invalid 1 test, 1 ok, 0 failed, 0 skipped ? 2 files, 2 tests, 2 ok, 0 failed, 0 skipped Time: 0 hour, 0 minute, 1 second |
1番後のファイルはtest_options.sh
であり1つのテストを実行しテストが成功したことを示しています。続いて、2番目のテストファイルはtest_sample.sh
であり1つのテストを実行しテストが成功したことを示しています。
最後に、2つのファイルで合計2つのテストを実行し2つともテストにパスしたことを示しています。テスト実行には1secの時間がかかったとなっています。
テスト(Test)
関数(Function)
テストは、test_
で始まる、または後述するAnnotationを使って指定します。テストは、テストファイルに書かれている順に実行されます。
function
キーワードはなくても問題ありません。以下は全てテスト対象となります。
1 2 3 4 5 6 7 8 9 10 11 12 |
#: @Test mytest() { echo "This is a test with Test annotation" } test_func() { echo "This is a test of the name which starts with 'test_'" } function test_func2() { echo "This is a test of the name which starts with 'test_'" } |
アノテーション(Annotation)
テスト関数の先頭に付与することができます。テストに関する補足情報を@に続く識別子で指定します。
@BeforeAll
すべてのテストに先立って実行される処理に指定します。
@BeforeEach
各テストの最初に実行される処理に指定します。
@Test[(…)]
テストに指定します。(…)がある場合は、テスト結果に表示されるテスト名が...
の部分で置換されます。
@Ignore
テストを無視します。
@Deprecated[(…)]
テストが非推奨であることを示します。テストの実行結果でDeprecated
という識別子が付加されます。
@TODO[(…)]
テストがTODOテストであることを示します。TAP用に用意されたものです。
@AfterEach
各テストの終了後に呼ばれます。テストに失敗しても実行されます。
@AfterAll
すべてのテストの最後に呼ばれます。
コマンドの実行
run <command>
外部コマンドやスクリプトの実行は、run
コマンドを利用できます。run
コマンドの実行結果(標準出力とエラー出力)は、$result
という変数に格納されます。また終了ステータスは、$status
という変数で参照可能です。また、出力の各行は$lines
という変数で配列として参照できます。$lines
には、出力が改行(\n
)区切りで格納されています。
1 2 3 4 5 6 |
#: @Test function test_run() { run echo "usage: baut" [ $status -eq 0 ] [[ "$result" =~ ^usage:[[:space:]]baut$ ]] } |
run2 <command>
こちらもrunと同様です。run2
は、標準出力とエラー出力がそれぞれ別々の変数で参照できます。変数は、$stdout
、$stderr
です。また各行は、$stdout_lines
、$stderr_lines
で参照できます。
1 2 3 4 5 6 |
#: @Test function test_run2() { run2 echo "usage: baut" [ $status -eq 0 ] [[ "$stdout" =~ ^usage:[[:space:]]baut$ ]] } |
eval2 <command>
コマンドをeval
で実行します。run2
と同様に標準出力とエラー出力結果を別々の変数で参照することができます。
1 2 3 4 5 6 |
#: @Test function test_eval2() { eval2 'echo "usage: baut" >&2' [ $status -eq 0 ] [[ "${stderr_lines[0]}" =~ ^usage:[[:space:]]baut$ ]] } |
テストのスキップ
skip [<message>]
関数の処理内で以降の処理をスキップする場合に使用します。以下では、最終行のecho
は実行されません。
1 2 3 4 5 6 |
#: @Test function test_skip() { eval2 'echo "usage: baut" >&2' skip "bye bye" echo "this message does not be printed" } |
実行すると以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 |
$ baut run test_sample2.sh 1 file, 4 tests [1] /Users/guest/workspace/baut/test/test_sample2.sh o test_run o test_run2 o test_eval2 ~ test_skip # skip: bye bye 4 tests, 3 ok, 0 failed, 1 skipped ? 1 file, 4 tests, 3 ok, 0 failed, 1 skipped Time: 0 hour, 0 minute, 0 second |
テストの失敗
fail [<message>]
終了ステータスコード1でそのテストを終了します。
1 2 3 |
function test_fail() { fail "not implementation" } |
ファイルのロード
load <file> [<arg>…]
ファイルをsource
コマンドでインクルードします。一度読み込まれたファイルも再度ロード可能です。ファイルが存在しない場合は、エラー終了(exit)します。
load_if_exists <file> [<arg> …]
ファイルをsource
コマンドでインクルードします。一度読み込まれたファイルも再度ロード可能です。ファイルが存在しない場合は、終了ステータスコード0
以外を返します。exit
しません。
require <file> [<arg> …]
ファイルをsource
コマンドでインクルードします。一度読み込まれたファイルは、重複ロードされません。ファイルが存在しない場合は、エラー終了(exit
)します。
その他API
ヘルパー(Helpers)
追加機能のことを指します。
diff-helper
run_diff
、run_diffx
といったコマンドを追加します。run
コマンドでは、通常、コマンドの実行結果を$result
や$lines
、$status
といった変数を用いて期待結果を比較しますが、このヘルパーではコマンドの実行結果をファイルに出力し、あらかじめ作成しておいた期待結果ファイルと比較を行ないます。
以下は、PostgreSQLにおけるリグレッションテストを模したテストになります。run_diffx
は、コマンド実行結果で差分があれば、そこでテストがエラーとなります。run_diff
は、テストはエラーとならず継続します。diffに失敗したか否かは$status
で確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
load "diff-helper" #: @BeforeEach setup() { export PGDATABASE=postgres psql -c "create table foo (id int, name text);" psql -c "insert into foo (id, name) values (1, 'bar');" } test_diff() { run_diffx psql -c "SELECT id FROM foo WHERE name = 'bar';" } #: @AfterEach teardown() { psql -c "drop table foo;" } |
ここでテストファイルが格納されるディレクトリとして、results
、expected
というディレクトリが作成されます。expected
には、テスト関数名+.out
という名前で期待結果ファイルを作成します。
1 2 3 4 5 6 7 |
$ tree expected results/ expected └── test_diff.out results/ └── test_diff.out 0 directories, 2 files |
差分が検出された場合は、テストエラーになります。続いて、results
ディレクトリ下に.diff
ファイルが作成されます。差分を確認して誤りを修正します。
内部構造(Baut Internal)
run〜レポート出力(From run to print report)
Bautはrunコマンドが実行されると、引数で指定されたテストファイルを読み込み、テストで実行される関数情報を収集します(compile)。この時、Annotationなどの情報を解釈します。続いて、読み込んだ内容からテスト実行ファイル(.baut
ファイル)を生成します。テスト実行ファイルは、Bautが一連のテストを実行するのに必要な構造で出力します。そして、このファイルもまた単なるBashスクリプトです。このテスト実行ファイルは、run
コマンド実行時に一時ディレクトリに出力されテストの終了時に削除されます。
テスト実行における出力は、すべてレポートのためのスクリプトにパイプで送信されます。レポートスクリプトは、送信されてきた内容を解釈し、テストレポートを構築します。
実際にどのような形の実行ファイルが生成されているかを確認するには、compile
コマンドを実行します。生成される結果は標準出力に書き出されます。以下出力例になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
$ baut compile test_sample.sh #!/usr/bin/env bash # This source was generated by Baut 0.1.0-beta at 2017-09-27 22:32:54 #:@filepath=/Users/guest/workspace/baut/test/test_sample.sh #:@checksum=79c5fd255e186952f9434f2826319604 #:@testcount=1 readonly BAUT_TEST_FILE="/Users/guest/workspace/baut/test/test_sample.sh" readonly BAUT_TEST_FUNCTIONS=(parse_cli_options) readonly BAUT_LIBEXEC=${BAUT_LIBEXEC:-"/Users/guest/workspace/github/baut/libexec"} [ -e "$BAUT_TEST_FILE" ] || abort "There is something wrong, file is not found: $BAUT_TEST_FILE" source "$BAUT_TEST_FILE" || abort "There is something wrong, failed to source '$BAUT_TEST_FILE'" require "baut--test-behavior" && load_if_exists "local--test-behavior" ||: baut_run_test_suit() { declare -ar before_all_functions=() declare -ar before_each_functions=() declare -ar after_all_functions=() declare -ar after_each_functions=() local BAUT_TEST_FUNCTION_NAME= baut_before_all # => 1: parse_cli_options BAUT_TEST_FUNCTION_NAME='parse_cli_options' (readonly BAUT_TEST_FUNCTION_NAME;baut_run_test "The usage should be displayed when command line options are invalid") BAUT_TEST_FUNCTION_NAME= baut_after_all } baut_start_test "baut_run_test_suit" |
run
コマンドでは、この生成された実行ファイルをsource
でインクルードし実行します。source "$BAUT_TEST_FILE"
という行がありますが、この時点でテストファイルが読み込みされます。その後は、Bautがテストファイルに書かれた順番で逐次テストを実行していきます。
コンパイル及びテスト実行の単位は、ファイル単位です。テストファイルにおける環境変数やその他設定は、そのテストファイルのみで有効です。いずれのテストファイルでも共有で使用する設定がある場合は、共通設定ファイルとして切り出し、各種テストファイルの先頭でload
する必要があります。
レポートフォーマット(Report Format)
Bautでは、デフォルトでいつくかのレポートフォーマットを提供しています。独自でレポートフォーマットを作成したい場合は、以下のようにすることでカスタマイズ可能です。
- ファイル名「
local--test-report
」を以下のディレクトリのいずれかに設置します。探索は、1から優先的に行われます。- コマンドを実行したディレクトリ
- テスト実行ディレクトリ/helpers
- baut/libexec
- baut/helpers
例えば、出力行の先頭に時間を挿入したい場合は以下のようにします。
1 2 3 |
while IFS= read -r line; do echo "`date +'%Y-%m-%d %H:%M:%S'` $line" done |
レポートスクリプトは、baut-exec-test
からの出力を標準入力として受け取りますので、上記ではループで1行ずつ取り出し出力しています。このままでは、レポートのメタ行と出力が混合された形となるので、レポートスクリプトでは、特別な意味を持つ行を解釈して処理する必要があります。
特別な意味を持つ行とは、通常以下のようなフォーマットとなります。
1 |
#:<CODE>;[<arg>][\t<arg>...] |
例えば、テストの最初には以下のようなメッセージがレポートスクリプトに渡されます。
1 |
#:RDY;1 2 |
これは、READYの略で、「トータルで、1ファイル、2テストの実行を開始する」という意味となります。このメセージを処理し、正しくレポートを表示する責任はレポートスクリプトにあります。
振る舞い(Behavior)
テスト実行の振る舞いを記述しているのが、baut--test-behavior
になります。こちらは、テストの実行や終了、各種基本的なコマンド(run
、run2
など)を提供している機能になります。こちらも以下のようにすることでカスタマイズ可能です。
- ファイル名「
local--test-behavior
」を以下のディレクトリのいずれかに設置します。探索は、1から優先的に行われます。- コマンドを実行したディレクトリ
- テスト実行ディレクトリ/helpers
- baut/libexec
- baut/helpers
load--test-behavior
は、baut--test-behavior
の後にロードされます。例えば、skipで送られるメッセージは以下のようにオーバーライドしカスタマイズできます。
1 2 3 4 5 6 7 8 |
skip() { __baut_test_skip=1 _baut_disable_debug_trap pop_setopt baut_after_each baut_report_code_skip "${FUNCNAME[1]}" "!!!Skip!!!${1:-}" # !!!Skip!!!を追加 exit 0 } |
ダウンロード
ライセンス(License)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
MIT License Copyright (c) 2017 haikikyou Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |