#author("2020-02-17T01:52:27+09:00","default:haikikyou","haikikyou")
#author("2020-02-17T01:52:51+09:00","default:haikikyou","haikikyou")
[[moritetuのIT関連技術メモ]]

#contents

* baut [#v1037d10]

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


* インストール [#c72c7dc1]

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

#geshi(bash){{{
$ git clone https://github.com/moritetu/baut.git
$ cd baut
$ source install.sh
$ baut run test
}}}

パスが通っていれば、&code(){-h};でUsageを確認できる。

#geshi(bash){{{
$ 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]'

}}}

* テスト [#m553508e]

** テストプログラム [#e5df3fd3]

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


#geshi(bash){{{
#!/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() {
  : # 本ファイルのすべてのテスト実行後に呼ばれる
}
}}}
実行すると以下のようになる。

#geshi(bash){{{
$ 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
}}}

** テストスイートの実行 [#s403cc39]

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

#geshi(bash){{{
$ baut run testdir
}}}

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

#geshi(bash){{{
$ baut run -r testdir
}}}


** テスト実行前確認 [#h36b5b30]

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

#geshi(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() {
  :
}
}}}

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

#geshi(bash){{{
 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
}}}

&code(){()};内の数字は実行される数である。
** 特定のテストのみ実行 [#e7a98121]

&code(){--match};オプションを使って実行する。

#geshi(bash){{{
$ 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" ]
}
}}}

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

#geshi(bash){{{
# 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メッセージでその旨が実行される。
* 特徴・機能 [#cda5c009]

** コマンド [#g817c193]

*** run [#y9f374d2]

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

|~変数|~説明|h
|result|stdoutとstderrの結果|
|status|コマンドの終了ステータス|
|lines|stdoutとstderrの結果の行単位の配列|

*** run2 [#u2556e0b]

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

|~変数|~説明|h
|result|stdoutの結果|
|status|コマンドの終了ステータス|
|lines|stdoutの結果の行単位の配列、stdout_linesと同じ|
|stdout|stdoutの結果|
|stderr|stderrの結果|
|stdout_lines|stdoutの結果の行単位の配列|
|stderr_lines|stderrの結果の行単位の配列|

#geshi(bash){{{
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 [#b6d06a17]

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

*** wait_until [#w50e20d3]

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

#geshi(bash){{{
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 [#jb69ffb7]

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

テストスイート全体で失敗時にテストを停止したい場合は、&code(){baut r -s};を実行する。~



*** skip [#t06affb7]

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

*** fail [#we9d1b38]

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

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

test_fail() {
    fail "Not implementation"
}

test_stop() {
    stop "halt test"
}
}}}
** _setup、_cleanup [#j516250c]

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

''ディレクトリ構成例''

#geshi(bash){{{
/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''

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

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

&code(){_all.sh};でないファイルを指定したい場合は、&code(){baut r --wrap-script <file>};で指定可能。~
このファイルは、テスト実行の最初にインクルードされる。~
そのため、&code(){_setup};関数はあるものの関数の外に記述したコマンドは初回に評価される。
** アノテーション [#de766abc]

*** @BeforeAll [#hb79de8d]

 #: @BeforeAll

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

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

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

*** @BeforeEach [#zd0ef13e]

 #: @BeforeEach

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

#geshi(bash){{{
#: @BeforeEach
setup1() {
  touch flagfile
}

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


*** @Test [#p4621079]
 
 #: @Test[(<text>)]

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

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

 #: @TODO[(<text>)]

TODOであることを示す。~
&code(){@TODO};のついた関数はテスト対象と見なされる。~
テストレポートに&code(){# TODO <text>};というマークをつけることができる。
*** @Ignore [#l9a05aa0]

 #: @Ignore

テスト対象から除外される。~
テストレポートにも表示されない。
*** @Deprecated [#n911479f]

 #: @Deprecated[(<text>)]

非推奨であることを示す。~
また、関数はテスト対象関数としてみなされる。~
&code(){# DEPRECATED <text>};というメッセージがレポートに出力される。
*** @AfterEach [#cb1b1414]

 #: @AfterEach

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

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

*** @AfterAll [#x83d531a]

 #: @AfterAll

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

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


*** @., @source, @include [#b4b4a62c]

#geshi(bash){{{
#: @.(test_a.sh)
#: @.(test_b.sh)
#: @.(test_c.sh)
}}}

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

''test_main.sh''

#geshi(bash){{{
#: @.(test_b.sh)
#: @.(test_c.sh)
}}}

''test_b.sh''

#geshi(bash){{{
test_b() {
    [ 1 -eq 1 ]
}
}}}

''test_c.sh''

#geshi(bash){{{
test_c() {
    [ 1 -eq 1 ]
}
}}}

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

#geshi(bash){{{
$ 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 [#def648e4]

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

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

- .
- helpers/
- baut/libexec
- baut/helpers

*** diff-helper [#bf411a26]

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

#geshi(bash){{{
load "diff-helper.sh"
}}}

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

**** run_diff [#kb6d1277]

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

#geshi(bash){{{
$ tree .
.
├── expected
│   └── foo.out
├── results
└── test_foo.sh

2 directories, 2 files
}}}

''foo.out''

#geshi(bash){{{
$ cat expected/foo.out
bar
}}}

実行。

#geshi(bash){{{
$ 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
}}}

差分は以下のとおり。

#geshi(bash){{{
$ 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の略) [#hb2a84ff]

&code(){run_diff};と同じであるが、diffの結果が不一致であった場合、現在実行しているテストは終了する。~
&code(){run_diff};は、とりあえず一度すべて流してdiffを一通り確認したい場合に有用。
**** begin_comparing、end_comparing [#le6fc997]

&code(){begin_comparing};と&code(){end_comparing};で囲まれた範囲で実行されて出力の結果をdiffの対象とする。~
複数のコマンドの実行結果をまとめて比較したい場合に有用。

#geshi(bash){{{
test_lines() {
    begin_comparing
    {
        echo "line1"
        echo "line2"
        echo "line3"
    }
    end_comparing
}
}}}
** ユーティリティ [#r60f6b9d]

*** __FILE__、__DIR__、__LINE__ [#q5f2d49d]

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

#geshi(bash){{{
$ tree test
test
└── test_util.sh

0 directories, 1 file
}}}

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

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

*** self [#n527c883]

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

*** load、load_if_exists [#jdd32392]

ヘルパーをロードする。

*** require [#cb09824b]

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

*** resolve_link [#g9c912a7]

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

*** push_load_path、pop_load_path [#ycb5374a]

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

*** datetime [#r041aabf]

&code(){%Y-%m-%d %H:%M:%S};の形式で日付を表示する。

*** log_xxx [#y495ea96]

ログを表示する。

#geshi{{{
log_trace <message>
log_debug <message>
log_info <message>
log_warn <message>
log_error <message>
}}}
** テストテンプレート [#w0da3b81]

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

#geshi(bash){{{
# baut init -h
$ 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.
}}}

以下のように実行する。

#geshi(bash){{{
$ 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
}}}
** テストドキュメント [#t5f97146]

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

#geshi(bash){{{

test_equal() {
  [ 1 -eq 1 ]
}

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

&code(){baut help};コマンドで出力することができる。

#geshi(bash){{{
$ baut help test_desc.sh
This test description.
  description
  description
}}}

ヘルプブロックは、以下のように&code(){#=begin HELP};と&code(){#=end HELP};で囲まれた箇所である。

#geshi{{{
#=begin HELP
#
# beginとendに囲まれたブロックがヘルプブロックになる。
# テストファイル内のどこに記述してもよい。
#
#=end HELP
}}}
* 参考リンク [#g7790abf]

- https://github.com/moritetu/baut
- https://baut.readthedocs.io/en/latest/

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