baut

更新日: 2020-05-10 (日) 00:08:46 (399d)

moritetuのIT関連技術メモ

baut

Bashで書かれたテストツール。
batsのようにコマンドラインベースでプログラムの振る舞いをテストすることを目的としている。
テストプログラム自体もすべてBashであり、ツール特有のシンタックスはない。
XUnitのような感覚で使える。

インストール

任意の場所にソース一式をダウンロードして、binディレクトリにパスを通すだけである。
install.shを実行すれば同じことをしてくれる。

$ git clone https://github.com/moritetu/baut.git
$ cd baut
$ source install.sh
$ baut run test

パスが通っていれば、-hでUsageを確認できる。

$ 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.
  init       Generates test files from a template.
  report     Format the result of 'baut test' execution.
  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]'

テスト

テストプログラム

  • テストファイルは、拡張子が*.shである。
  • テストファイル名は、test_で始まる必要がある。
  • テスト単位は、シェルの関数であり、test_で始まる関数はテスト対象である。
    test_で始まる関数名でなくともアノテーション@Testが付与されている関数はテスト対象となる。
  • #:は特別な意味を持ち、#: の後には@アノテーションなどを記述する。
  • ファイル内のテストは上から順に実行される。
    コマンドがエラーで終了するとテストは失敗する。
    失敗しても回避したい場合は、<command> || status=$? などでステータスを一時保存しればよい。
#!/usr/bin/env bash

#: @BeforeAll
function setup_all() {
  : # 本ファイルのすべてのテスト実行前に一度呼ばれる
}

#: @BeforeEach
function setup() {
  : # 各テスト前に一度呼ばれる
}

#: @Test(テストの内容について記述できる)
test_ng_sample() {
  fail "Not implemented"
}

#: @Test
test_ng_sample2() {
  run echo "bar"
  [ $status -ne 0 ] || fail "exit status should not be 0, but '$status'" "result: $result"
}

#: @Test
test_ok_sample() {
  run echo "hello baut"
  [ "$result" = "hello baut" ]
  [ $status -eq 0 ]
}

#: @Test
test_skip_sample() {
  run echo "hello baut"
  skip "Good bye!"
  echo "Not reach here"
}

#: @Test
test_wait_until() {
  local pidfile="$(__DIR__)/sample.pid"
  eval "sleep 2 && echo $BASHPID > $pidfile" &
  wait_until --retry-max 3 "[ -e '$pidfile' ]"
  rm $pidfile ||:
}

#: @AfterEach
function teardown() {
  : # 各テストの実行後に呼ばれる
}

#: @AfterAll
function after_all() {
  : # 本ファイルのすべてのテスト実行後に呼ばれる
}

実行すると以下のようになる。

$ baut r test_sample.sh
1 file, 5 tests
#1 /tmp/test_sample.sh
x テストの内容について記述できる
  Not implemented
  # Error(1) detected at the following:
  #       13    #: @Test(テストの内容について記述できる)
  #       14    test_ng_sample() {
  #=>     15      fail "Not implemented"
  #       16    }
  #       17
x test_ng_sample2
  exit status should not be 0, but '0'
  result: bar
  # Error(1) detected at the following:
  #       19    test_ng_sample2() {
  #       20      run echo "bar"
  #=>     21      [ $status -ne 0 ] || fail "exit status should not be 0, but '$status'" "result: $result"
  #       22    }
  #       23
o test_ok_sample
~ test_skip_sample # SKIP Good bye!
o test_wait_until
#$ 5 tests, 2 ok, 2 failed, 1 skipped

💥  1 file, 5 tests, 2 ok, 2 failed, 1 skipped
Time: 0 hour, 0 minute, 3 seconds

テストスイートの実行

ディレクトリを指定して実行すれば、ディレクトリ下のテストを順に実行する。
テストファイルの実行順序は決まっていない。

$ baut run testdir

2階層以上にもテストファイルが存在する場合は、-rをつける。

$ baut run -r testdir

テスト実行前確認

実行されるテストについて、テスト実行前に確認できる。

#: @BeforeAll
function setup_all() {
  :
}

#: @BeforeEach
function setup() {
  :
}

#: @Test
test_ng_sample() {
  fail "Not implemented"
}

#: @Test
test_ng_sample2() {
  run echo "bar"
  [ $status -ne 0 ] || fail "exit status should not be 0, but '$status'" "result: $result"
}

#: @Test
test_ok_sample() {
  run echo "hello baut"
  [ "$result" = "hello baut" ]
  [ $status -eq 0 ]
}

#: @Test
test_skip_sample() {
  run echo "hello baut"
  skip "Good bye!"
  echo "Not reach here"
}

#: @Test
test_wait_until() {
  local pidfile="$(__DIR__)/sample.pid"
  eval "sleep 2 && echo $BASHPID > $pidfile" &
  wait_until --retry-max 3 "[ -e '$pidfile' ]"
  rm $pidfile ||:
}

#: @AfterEach
function teardown() {
  :
}

#: @AfterAll
function after_all() {
  :
}

実行される関数は以下のとおりである。

 baut r -d test_sample.sh
[1] /test/test_sample.sh
 ├─  (1) before_all_functions => setup_all
 ├─  (1) before_each_functions => setup
 ├─  (5) test_functions => test_ng_sample test_ng_sample2 test_ok_sample test_skip_sample test_wait_until
 ├─  (1) after_each_functions => teardown
 └─  (1) after_all_functions => after_all

()内の数字は実行される数である。

特定のテストのみ実行

--matchオプションを使って実行する。

$ cat test_match.sh
#: @Test
param_test1() {
  test 1 -eq 1
}

#: @Test
param_test2() {
  test 1 -eq 1
}

#: @Test
command_test1() {
  run echo "hoge"
  [ "$stdout" =  "hoge" ]
}

#: @Test
command_test2() {
  run echo "foo"
  [ "$stdout" = "foo" ]
}

実行すると以下のようになる。

# baut run --match "param_*" test_match.sh
1 file, 4 tests
#1 /test/diff/test_match.sh
o param_test1
o param_test2
#$ 4 tests, 2 ok, 0 failed, 0 skipped

# WARNING planned tests were not executed absolutely!
🎉  1 file, 2 tests, 2 ok, 0 failed, 0 skipped
Time: 0 hour, 0 minute, 0 second

ファイル内のすべてのテストが実行されていないので、WARNINGメッセージでその旨が実行される。

特徴・機能

コマンド

run

後に続くコマンドを実行し、結果を変数に格納する。

変数説明
resultstdoutとstderrの結果
statusコマンドの終了ステータス
linesstdoutとstderrの結果の行単位の配列

run2

後に続くコマンドを実行し、結果を変数に格納する。
runと違うのは、stdoutとstderrを別々に扱うこと。

変数説明
resultstdoutの結果
statusコマンドの終了ステータス
linesstdoutの結果の行単位の配列、stdout_linesと同じ
stdoutstdoutの結果
stderrstderrの結果
stdout_linesstdoutの結果の行単位の配列
stderr_linesstderrの結果の行単位の配列
myfunc() {
  echo "hoge"
  echo "bar" >&2
  exit 1
}

test_run() {
    run echo "hoge"
    [ "$result" = "hoge" ]
    [ "${lines[0]}" = "hoge" ]
    (( status == 0 ))
}

test_run2() {
    run2 myfunc
    [ "$result" = "hoge" ]
    [ "${stdout_lines[0]:-}" = "hoge" ]
    [ "${stderr_lines[0]}" = "bar" ]
    [ "$stdout" = "hoge" ]
    [ "$stderr" = "bar" ]
    (( status == 1 ))
}

eval2

run2と同じ、リダイレクトやパイプなどを含むコマンドを実行する。

wait_until

実行に時間のかかるコマンドを待ち合わせる。

test_wait_until() {
  local pidfile="$(__DIR__)/sample.pid"
  eval "sleep 2 && echo $BASHPID > $pidfile" &
  wait_until --retry-max 3 "[ -e '$pidfile' ]"
  test -e "$pidfile" && rm "$pidfile" ||:
}

stop

テストファイル内での以降のテストプロセスを停止する。

テストスイート全体で失敗時にテストを停止したい場合は、baut r -sを実行する。

skip

テストをスキップする。
実行しているテストがスキップされ、次のテストに移る。

fail

テストを失敗させる。
実行しているテストは直ちに失敗となり、次のテストに移る。

test_skip() {
    skip "This test is skipped"
    echo "not reach here"
}

test_fail() {
    fail "Not implementation"
}

test_stop() {
    stop "halt test"
}

_setup、_cleanup

テスト全体で一度のみ実行したい処理は、_all.shというファイルに記述する。
これは、テスト実行ディレクトリのtopにおいておく必要がある。
_setup_cleanupは、テスト実行前、テスト実行後、にそれぞれ一回のみ実行される。
複数のテストファイルがある場合、一度のみ実行される。

ディレクトリ構成例

/path/to/test_root
 |- all.sh    # test_rootでテストをrunすれば有効
 |- test_hoge.sh
 |- test_parameters
   |- test_get.sh
   |- test_post.sh
   |- _all.sh # これは実行されない、test_parametersでテストをrunすれば有効

_all.sh

#!/usr/bin/env bash
# _all.sh
#
_setup() {
  echo "called at the begin of test suite"
}

_cleanup() {
  echo "called at end begin of test suite"
}

_all.shでないファイルを指定したい場合は、baut r --wrap-script <file>で指定可能。
このファイルは、テスト実行の最初にインクルードされる。
そのため、_setup関数はあるものの関数の外に記述したコマンドは初回に評価される。

アノテーション

@BeforeAll

#: @BeforeAll

テストファイル内のテスト関数実行前に一度のみ呼ばれる。
複数定義している場合は、順に実行される。

# (1)
#: @BeforeAll
setup_all1() {
  GLOBAL_VAR1=10
}

# (2)
#: @BeforeAll
setup_all2() {
  export PATH=/usr/local/bin:"$PATH"
}

@BeforeEach

#: @BeforeEach

各テスト関数実行前に逐次実行される。
複数定義がある場合は、順に実行される。

#: @BeforeEach
setup1() {
  touch flagfile
}

#: @BeforeEach
setup2() {
  TEST_VAR2=20
}

@Test

#: @Test[(<text>)]

テスト対象関数であることを示す。
test_で始まる関数は自動的にテスト関数と見なすが、そうでない場合でも@Testをつければテスト対象となる。
また、テスト実行レポートに出力されるメッセージはテスト関数名であるが、<text>を指定することでメッセージを任意の文字列に置換できる。

#: @Test(This test should be absolutely passed)
test_passed() {
  [ 1 -eq 1 ]
}

@TODO

#: @TODO[(<text>)]

TODOであることを示す。
@TODOのついた関数はテスト対象と見なされる。
テストレポートに# TODO <text>というマークをつけることができる。

@Ignore

#: @Ignore

テスト対象から除外される。
テストレポートにも表示されない。

@Deprecated

#: @Deprecated[(<text>)]

非推奨であることを示す。
また、関数はテスト対象関数としてみなされる。
# DEPRECATED <text>というメッセージがレポートに出力される。

@AfterEach

#: @AfterEach

テスト実行後に逐次実行される。

#: @AfterEach
teardown() {
  rm flagfile ||:
}

@AfterAll

#: @AfterAll

ファイル内のすべてのテスト実行後に実行される。

#: @AfterAll
teardown_all() {
  rm "$TMPDIR/*.tmp" ||:
}

@., @source, @include

#: @.(test_a.sh)
#: @.(test_b.sh)
#: @.(test_c.sh)

複数のテストに分割した場合に、他のテストをインクルードする。
テスト結果は、インクルード元にカウントされる。

test_main.sh

#: @.(test_b.sh)
#: @.(test_c.sh)

test_b.sh

test_b() {
    [ 1 -eq 1 ]
}

test_c.sh

test_c() {
    [ 1 -eq 1 ]
}

実行結果は以下のとおり。

$ baut r test_main.sh
1 file, 2 tests
#1 /test/test_main.sh
o test_b
o test_c
#$ 2 tests, 2 ok, 0 failed, 0 skipped

🎉  1 file, 2 tests, 2 ok, 0 failed, 0 skipped
Time: 0 hour, 0 minute, 0 second

helper

テストの実行を支援する機能を追加したい場合に使用する。
loadコマンドを使ってロードする。

以下のパスにあるfileが検索対象となる。

  • .
  • helpers/
  • baut/libexec
  • baut/helpers

diff-helper

diffを使って期待結果ファイルと実行結果を比較する機能を提供する。

load "diff-helper.sh"

これにより、以下の関数が使用可能となる。
期待結果ファイルは、expected/<関数名>.out である。
実際の結果は、results/<関数名>.out である。
差分があった場合は、results/<関数名>.out.diff ファイルが生成される。

run_diff

diffの結果、失敗しても処理を継続する。

$ tree .
.
├── expected
│   └── foo.out
├── results
└── test_foo.sh

2 directories, 2 files

foo.out

$ cat expected/foo.out
bar

実行。

$ baut r test_foo.sh
1 file, 1 test
#1 /test/diff/test_foo.sh
o foo
  See /test/diff/results/foo.out.diff
#$ 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

差分は以下のとおり。

$ cat /test/diff/results/foo.out.diff
--- /test/diff/expected/foo.out    2020-02-16 01:34:49.390773740 +0900
+++ /test/diff/results/foo.out     2020-02-16 01:37:46.481055289 +0900
@@ -1 +1 @@
-bar
+foo
run_diffx(run diff and exitの略)

run_diffと同じであるが、diffの結果が不一致であった場合、現在実行しているテストは終了する。
run_diffは、とりあえず一度すべて流してdiffを一通り確認したい場合に有用。

begin_comparing、end_comparing

begin_comparingend_comparingで囲まれた範囲で実行されて出力の結果をdiffの対象とする。
複数のコマンドの実行結果をまとめて比較したい場合に有用。

test_lines() {
    begin_comparing
    {
        echo "line1"
        echo "line2"
        echo "line3"
    }
    end_comparing
}

ユーティリティ

__FILE__、__DIR__、__LINE__

ファイル、ディレクトリ、行番号を表示する。

$ tree test
test
└── test_util.sh

0 directories, 1 file

パスするテストは以下のようになる。

test_util() {
    [ `__FILE__` = "/test/test_util.sh" ]
    [ `__DIR__` = "/test" ]
    [ `__LINE__` -eq 4 ]
}

self

現在実行中の関数名を示す。

load、load_if_exists

ヘルパーをロードする。

require

既にロード済みの場合は、ロードしない。

resolve_link

シンボリックリンクを解決し絶対パスを返す。

push_load_path、pop_load_path

loadの検索対象パスに加える、削除する。

datetime

%Y-%m-%d %H:%M:%Sの形式で日付を表示する。

log_xxx

ログを表示する。

log_trace <message>
log_debug <message>
log_info <message>
log_warn <message>
log_error <message>

テストテンプレート

デフォルトのテストスタイルを定義できる。
チームで統一した場合などは、templateディレクトリにひな形を用意しておくことで、initコマンドでプロジェクトを初期化できる。

$ baut init -h
Usage: baut init [-t <template>] [-f] [-i] <outdir>

Generates test files from a template. 'init' just copies files or directories
with the specified template.

OPTIONS
  -t, --template <template>
    Copies from a specified template.

  -i
    Shows available templates.

  -f
    Overwrites.

  -DVARNAME=VALUE
    Passes the specified variables to place holders.

以下のように実行する。

$ baut init -i mongo
[Available Templates]
 default
 mongo
 mysql
 postgresql
 redis

$ baut init -t mongo mongo
$ tree mongo
mongo/
├── _all.sh
├── expected
│   └── test_query.out
├── run-test.sh
└── test_sample.sh

1 directory, 4 files

テストドキュメント

テストファイル内に説明を加えることができる。

test_equal() {
  [ 1 -eq 1 ]
}

#=begin HELP
#
# This test description.
#   description
#   description
#
#=end HELP

baut helpコマンドで出力することができる。

$ baut help test_desc.sh
This test description.
  description
  description

ヘルプブロックは、以下のように#=begin HELP#=end HELPで囲まれた箇所である。

#=begin HELP
#
# beginとendに囲まれたブロックがヘルプブロックになる。
# テストファイル内のどこに記述してもよい。
#
#=end HELP

参考リンク


トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
目次
TOP | 閉じる | ダブルクリックで閉じる