ShellSpecとは
- シェルスクリプト用の BDD テストフレームワーク
- テスト前後のフック、テストのグループ化・階層化、モック、パラメータ化テスト、カバレッジ計測など、豊富な機能を持つ
- shUnit2, Bats-core との比較表 > ShellSpec | BDD unit testing framework for shell scripts (bash, ksh, zsh, dash and all POSIX shells)
- 日本の方(@ko1nksm)が作られており、Qiitaにも紹介記事があるので読んでおくとよさげ
環境
- 以下のコマンドで準備した Ubuntu 上
# Ubuntu のコンテナを起動
> docker run -it --name shell-spec-test ubuntu:22.10
# ShellSpec のインストールに必要なツールをインストール
$ apt-get update
$ apt-get install -y wget git
※ $
って書いてるけど、以後のコマンドも含めすべて root ユーザで実行してる(コメント行と区別しやすくするため)
インストール
# ShellSpec のインストール
$ wget -O- https://git.io/shellspec | sh
# PATHが通ってるところに shellspec のシンボリックリンクを配置
$ ln -s /root/.local/lib/shellspec/shellspec /usr/local/bin/
# ShellSpec のバージョン
$ shellspec -v
0.28.1
- 任意の場所に手動インストールしたい場合は、 こちら を参照
- git のリポジトリ落としてきてリンクつくればいいっぽい
Hello World
# 作業ディレクトリを作成
$ mkdir -p /work/hello-world
$ cd /work/hello-world
# ShellSpec のプロジェクトを作成
$ shellspec --init
create /work/hello-world/.shellspec
create /work/hello-world/spec/spec_helper.sh
フォルダ構成・実装
/work/hello-world
|-spec/
| |-hello_spec.sh *
| `-spec_helpler.sh
|-lib/ *
| `-hello.sh *
`-.shellspec
* がついてるのは追加で作成したディレクトリ・ファイル
hello.sh
hello() {
echo "Hello ${1}?"
}
hello_spec.sh
HOGE=hoge
Describe 'hello.sh'
Include lib/hello.sh
echo "HOGE=${HOGE}"
It 'says hello'
When call hello ShellSpec
The output should equal 'Hello ShellSpec!'
End
End
実行結果
$ shellspec
HOGE=hoge
Running: /bin/sh [sh]
F
Examples:
1) hello.sh says hello
When call hello ShellSpec
1.1) The output should equal Hello ShellSpec!
expected: "Hello ShellSpec!"
got: "Hello ShellSpec?"
# spec/hello_spec.sh:11
Finished in 0.03 seconds (user 0.03 seconds, sys 0.00 seconds)
1 example, 1 failure
Failure examples / Errors: (Listed here affect your suite's status)
shellspec spec/hello_spec.sh:8 # 1) hello.sh says hello FAILED
説明
/work/hello-world
|-spec/
| |-hello_spec.sh *
| `-spec_helpler.sh
|-lib/ *
| `-hello.sh *
`-.shellspec
- ShellSpec は、プロジェクトディレクトリの直下で実行できる
-
.shellspec
が配置されたディレクトリが、プロジェクトディレクトリのルートとなる(プロジェクトルート)-
.shellspec
には、デフォルトで適用するオプションを記載できる - デフォルトで適用したいオプションが無くても、空ファイルを配置しておく必要がある
- 0.28.0 より前はファイルがなくても動いていたけど、 0.28.0 以降はファイルの存在がチェックされるようになった
-
- テスト仕様を書いたファイルは
spec
ディレクトリの下に配置する- デフォルトでは、
spec
ディレクトリの下の_spec.sh
で終わるファイルがテスト仕様と見なされる(変更可能。詳細後述)
- デフォルトでは、
-
spec/spec_helper.sh
は、すべてのテストで共通する設定をしたり、すべてのテストから参照できるグローバルな関数を定義したりするのに使用する(詳細後述)
HOGE=hoge
Describe 'hello.sh'
Include lib/hello.sh
echo "HOGE=${HOGE}"
It 'says hello'
When call hello ShellSpec
The output should equal 'Hello ShellSpec!'
End
End
- テスト仕様は、独自の DSL で記述する
- このテスト仕様は、実行前にシェルスクリプトに変換されて実行される
-
shellspec --translate
を実行すると、変換後の状態が確認できる
-
- 文法はシェルスクリプトと互換性があるらしい
- なので、テスト仕様のファイル自体を ShellCheck にかけることができたりする
- 任意のシェルコマンドを途中に記述することが可能
- ただし、スコープが普通のシェルスクリプトとかと違う(
Describe
でスコープが分けられてたりする)ので注意
- ただし、スコープが普通のシェルスクリプトとかと違う(
実行シェル
$ shellspec
Running: /bin/sh [sh]
...
- デフォルトでは、
/bin/sh
を使ってテストが実行される - 任意のシェルで実行したい場合は、
-s
オプションを指定する
$ shellspec -s bash
Running: /usr/bin/bash [bash 5.2.2(1)-release]
...
- 毎回指定するのが面倒な場合は、
.shellspec
に記載しておく
.shellspec
--require spec_helper
-s bash
実行結果
$ shellspec
Running: /usr/bin/bash [bash 5.2.2(1)-release]
...
実行対象のテスト仕様
フォルダ構成
<PROJECT-ROOT>
|-.shellspec
`-spec/
|-spec_helper.sh
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
spec/test1_spec.sh
echo "test1_spec"
spec/subdir/test2_spec.sh
echo "test2_spec"
実行結果
$ shellspec
test2_spec
test1_spec
- デフォルトでは、プロジェクトルート直下の
spec
ディレクトリの下にある_spec.sh
で終わるファイルが全て実行対象となる - ディレクトリはサブディレクトリも再帰的に検索される
実行対象のテスト仕様を格納するディレクトリを変える
フォルダ構成
<PROJECT-ROOT>
|-.shellspec
|-spec/
| `-spec_helper.sh
`-test/
|-test1.sh
|-no-test.sh
`-subdir/
`-test2.sh
-
spec
ではなく、test
ディレクトリの下にテスト仕様のファイルを配置している - テスト対象ではないファイルとして
no-test.sh
も置いている -
spec_helper.sh
はspec
ディレクトリの下に置いたままにしている-
spec_helper.sh
の配置先は別の設定でspec
ディレクトリが指定されているため、これはこのままにしておく必要がある - 場所は設定で変更可能(詳細後述)
-
test/test1.sh
echo "test1"
test/no-test.sh
echo "no-test"
test/subdir/test2.sh
echo "test2"
実行結果
$ shellspec --default-path test --pattern '**/test*.sh'
test2
test1
-
--default-path
で、テスト仕様を読み込む対象となるディレクトリを指定する -
--pattern
で、対象となるテスト仕様のファイル名のパターンを指定する- ここでは、「任意のサブディレクトリにある
test
で始まり拡張子が.sh
であるファイル」を対象にしている
- ここでは、「任意のサブディレクトリにある
複数のディレクトリにテスト仕様のファイルを格納する
フォルダ構成
<PROJECT-ROOT>
|-.shellspec
|-spec/
| `-spec_helper.sh
|-module1/
| `-spec/
| |-no-test.sh
| `-test-module1.sh
`-module2/
`-spec/
`-test-module2.sh
- テスト仕様のファイルを、各モジュールディレクトリ直下の
spec
ディレクトリに配置している
test-module1.sh
echo "test-module1"
no-test.sh
echo "no-test"
test-module2.sh
echo "test-module2
実行結果
$ shellspec --default-path '*/spec' --pattern '**/test*.sh'
test-module1
test-module2
-
--default-path
で、プロジェクトルート直下のサブディレクトリの直下にあるspec
ディレクトリを指定 -
--pattern
で、任意のディレクトリの下のtest
で始まり拡張子が.sh
であるファイルを指定している- ここで、
--pattern
にtest*.sh
と指定してもうまくいかない -
--pattern
で指定するパターンは、--default-path
からの相対パスではなく、プロジェクトルートからの相対パスに対してマッチングする - 例えば、
test-module1.sh
はmodule1/spec/test-module1.sh
としてマッチングされる - このため、任意のサブディレクトリを指す
**/
が先に必要となる
- ここで、
作業ディレクトリ
フォルダ構成
/work/execdir
|-.shellspec
`-spec/
|-spec_helper.sh
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
test1_spec.sh
echo "pwd=$(pwd) in spec dir"
test2_spec.sh
echo "pwd=$(pwd) in subdir"
実行結果
$ shellspec
pwd=/work/execdir in subdir
pwd=/work/execdir in spec dir
- 各テスト仕様実行時の作業ディレクトリは、テスト仕様の場所に関係なく常にプロジェクトルートになる(デフォルト)
作業ディレクトリを変更する
フォルダ構成
/work/execdir
|-.shellspec
|-src/ <--- NEW
`-spec/
|-spec_helper.sh
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
- プロジェクトルートに
src
というディレクトリを作って、そっちを作業ディレクトリにしてみる
実行結果
$ shellspec --execdir @project/src
pwd=/work/execdir/src in subdir
pwd=/work/execdir/src in spec dir
-
--execdir
オプションで、実行時の作業ディレクトリを指定できる - 作業ディレクトリの指定は、あらかじめ用意されたいくつかのロケーションからの相対パスで指定する
- ロケーションには、以下の3つが用意されている
-
@project
- プロジェクトルート
-
.shellspec
ファイルが配置されているディレクトリ - デフォルトはこれ
-
@basedir
- 実行中のテスト仕様のファイルが配置されているディレクトリから親ディレクトリにさかのぼっていき、最初に
.shellspec
または.shellspec-basedir
ファイルが見つかったディレクトリ
- 実行中のテスト仕様のファイルが配置されているディレクトリから親ディレクトリにさかのぼっていき、最初に
-
@specfile
- 実行中のテスト仕様のファイルが配置されているディレクトリ
-
- 作業ディレクトリは、必ずプロジェクトディレクトリの中のディレクトリしか指定できない
-
@project/..
とかしてもエラーになる
-
@basedir の場合
フォルダ構成
/work/execdir
|-.shellspec
`-spec/
|-.shellspec-basedir <--- NEW
|-spec_helper.sh
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
-
spec
ディレクトリ直下に.shellspec-basedir
ファイルを配置(空ファイル)
実行結果
$ shellspec --execdir @basedir
pwd=/work/execdir/spec in subdir
pwd=/work/execdir/spec in spec dir
@specfileの場合
フォルダ構成
/work/execdir
|-.shellspec
`-spec/
|-spec_helper.sh
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
実行結果
$ shellspec --execdir @specfile
pwd=/work/execdir/spec/subdir in subdir
pwd=/work/execdir/spec in spec dir
DSL の文法
Describe, Context, ExampleGroup
MESSAGE="default message"
echo "MESSAGE=${MESSAGE} @all begin"
Describe "hoge"
echo "MESSAGE=${MESSAGE} @hoge begin"
MESSAGE="hoge"
Context "fuga"
echo "MESSAGE=${MESSAGE} @fuga begin"
MESSAGE="fuga"
echo "MESSAGE=${MESSAGE} @fuga end"
End
ExampleGroup "piyo"
echo "MESSAGE=${MESSAGE} @piyo"
End
echo "MESSAGE=${MESSAGE} @hoge end"
End
echo "MESSAGE=${MESSAGE} @all end"
実行結果
MESSAGE=default message @all begin
MESSAGE=default message @hoge begin
MESSAGE=hoge @fuga begin
MESSAGE=fuga @fuga end
MESSAGE=hoge @piyo
MESSAGE=hoge @hoge end
MESSAGE=default message @all end
-
Describe
,Context
,ExampleGroup
を使うと example グループブロック(example group block)を定義できる- example グループブロックには、他の example グループブロックや、 example ブロックを含めることができる
- 要するに
Describe
やIt
を入れ子にできる、ということ
- 要するに
-
Describe
とContext
は、ExampleGroup
のエイリアス
- example グループブロックには、他の example グループブロックや、 example ブロックを含めることができる
-
Describe "グループの説明"
で開始し、End
で終了する - グループ内は独自のスコープになる
- スコープ外で定義された変数は参照できるが、スコープ内で設定した値はスコープを出ると参照できなくなる
It, Specify, Example
It "hello"
When call echo hello
The output should equal HELLO
End
実行結果
Examples:
1) hello
When call echo hello
1.1) The output should equal HELLO
expected: "HELLO"
got: "hello"
-
It
,Specify
,Example
を使うと、 example ブロック(example block)を定義できる(要するに、1つのテストケース)-
It
,Specify
は、Example
のエイリアス
-
-
It "説明"
で開始し、End
で終了する - example ブロックは、以下のもので構成される
- 1つの評価(evaluation)
- 上の例だと、
When
の行が該当する
- 上の例だと、
- 複数の期待結果(expectations)
- 上の例だと、
The
の行が該当する
- 上の例だと、
- 1つの評価(evaluation)
When
It "When"
When call echo hello
The output should equal hello
End
-
When
を使うことで、検証したいシェル関数やコマンドの実行を宣言できる-
When
は、It
で定義した example ブロックの中で使用する
-
-
When <call | run> <function | command> [arguments...]
という感じで指定する -
When call
は現在のシェルで関数またはコマンドを実行し、When run
はサブシェルで関数またはコマンドを実行する- シェルスクリプトの場合は
When call
でも別プロセスとして起動された
- シェルスクリプトの場合は
callとrunの違い
hoge() {
MESSAGE=hoge
echo 1
}
It "When call"
MESSAGE=hello
echo "before MESSAGE=${MESSAGE} @ When call"
When call hoge
The output should equal 1
echo "after MESSAGE=${MESSAGE} @ When call"
End
It "When run"
MESSAGE=hello
echo "before MESSAGE=${MESSAGE} @ When run"
When run hoge
The output should equal 1
echo "after MESSAGE=${MESSAGE} @ When run"
End
実行結果
before MESSAGE=hello @ When call
after MESSAGE=hoge @ When call
before MESSAGE=hello @ When run
after MESSAGE=hello @ When run
-
call
の方は同じシェルで hoge 関数が実行されるため、変数の変更が呼び出し元にも影響している
When run のバリエーション
-
When run
には、さらにいくつかのバリエーションがある
tesh.sh
#!/bin/bash
echo "MESSAGE=${MESSAGE} SHELL=${SHELL} @ test.sh"
test_spec.sh
MESSAGE="Hello World"
It "When run"
When run ./test.sh
The output should equal "test"
End
It "When run command"
PATH=${PATH}:/work/hello-world
When run command test.sh
The output should equal "test"
End
It "When run script"
When run script test.sh
The output should equal "test"
End
It "When run source"
When run source ./test.sh
The output should equal "test"
End
実行結果
Running: /bin/sh [sh]
.FFFF
Examples:
1) When run
When run ./test.sh
1.1) The output should equal test
expected: "test"
got: "MESSAGE= SHELL=/bin/bash @ test.sh"
# spec/test_spec.sh:5
2) When run command
When run command test.sh
2.1) The output should equal test
expected: "test"
got: "MESSAGE= SHELL=/bin/bash @ test.sh"
# spec/test_spec.sh:11
3) When run script
When run script test.sh
3.1) The output should equal test
expected: "test"
got: "MESSAGE= SHELL= @ test.sh"
# spec/test_spec.sh:16
4) When run source
When run source ./test.sh
4.1) The output should equal test
expected: "test"
got: "MESSAGE=Hello World SHELL= @ test.sh"
# spec/test_spec.sh:21
-
When run command ...
- シェルスクリプトまたはコマンドを実行する
- パスが通っている必要がある
- シェルスクリプトの場合、シバンで指定したシェルでスクリプトが実行される
- シェルスクリプトまたはコマンドを実行する
-
When run script ...
- シェルスクリプトを実行する
- シバンは無視され、 ShellSpec を実行しているシェルの別インスタンスで実行される
-
Whne run source ...
まとめると、以下のような感じ。
比較項目 | run | run command | run script | run source |
---|---|---|---|---|
シェル関数の実行 | ○ | × | × | × |
シェルスクリプトの実行 | ○ | ○ | ○ | ○ |
コマンドの実行 | ○ | ○ | × | × |
シバン | 有効 | 有効 | 無視 | 無視 |
変数等の継承 | × | × | × | ○ |
The
It "test"
When call echo hello
The output should equal hello
End
-
The
を使うことで、When
で実行した結果を検証できる -
The
は、以下のような文法で記述する
Theの文法
The [Modifiers...] <Subjects> should <Matchers> <expected value>
Subjects
The output should equal hello
- Subjects では、検証対象を指定する
- 上記例だと、
output
が Subject になる - Subjects には、以下のようなものが用意されている(非推奨となっているものは割愛)
Subject | 説明 |
---|---|
stdout / output
|
標準出力 |
line <N> |
標準出力のN行目を対象にする |
word <N> |
標準出力のN番目の単語を対象にする |
stderr / error
|
標準エラー出力 |
status |
終了ステータス |
path / file / directory
|
任意のパス |
function <関数名> / <関数名>()
|
関数を対象にする(Modifers の result と組み合わせて使うっぽい) |
variable <変数名> |
変数の値 |
stdout / output
It "test"
When call echo hello world
The output should equal "hello world"
End
line
myfunc() {
cat << EOS
hoge
fuga
piyo
EOS
}
It "test"
When call myfunc
The line 2 should equal "fuga"
End
word
It "test"
When call echo hello world
The word 2 should equal "world"
End
stderr / error
myfunc() {
echo hello >&2
}
It "test"
When call myfunc
The error should equal "hello"
End
status
myfunc() {
return 2
}
It "test"
When call myfunc
The status should equal 2
End
path / file / directory
It "test"
touch /work/hoge
The file /work/hoge should be exist
End
function
myfunc() {
echo hoge
}
It "test"
The function myfunc should equal myfunc
The result of function myfunc should equal hoge
The "myfunc()" should equal myfunc
The result of "myfunc()" should equal hoge
End
-
function
だけだと、ただ関数名を検証するだけになってよくわからないが、 Modifiers のresult
と組み合わせることで関数の結果を検証対象にできるようになる- カスタムの検証を行いたい場合に利用できるっぽい
-
<関数名>()
という省略した書き方ができる
variable
It "test"
MESSAGE=hoge
The variable MESSAGE should equal "hoge"
End
Modifiers
It "test"
When call echo hello world
The word 2 of output should equal "world"
End
- Modifiers は、 Subjects で指定した対象をさらに限定するために用いる修飾要素になる
- 上記例でいうと、
word 2 of
が該当する- output の2単語目に検証対象を絞り込んでいる
- 以下の Modifier が用意されている
Modifer | 説明 |
---|---|
line <N> |
Subjectの N 行目 |
lines |
Subjectの行数 |
word <N> |
Subjectの N 単語目 |
length |
Subjectの文字数 |
contents |
Subjectが指すファイルの内容 |
result |
Subjectが指す関数の実行結果 |
line
myfunc() {
cat << EOF
hoge
fuga
piyo
EOF
}
It "test"
When call myfunc
The line 2 of output should equal "fuga"
End
lines
myfunc() {
cat << EOF
hoge
fuga
piyo
EOF
}
It "test"
When call myfunc
The lines of output should equal 3
End
word
It "test"
When call echo hello world
The word 2 of output should equal "world"
End
length
It "test"
When call echo hello world
The length of output should equal 11
End
contents
It "test"
echo Hello World > test.txt
The contents of file test.txt should equal "Hello World"
End
result
myfunc() {
echo Hello World
}
It "test"
The result of function myfunc should equal "Hello World"
End
連鎖させる
myfunc() {
cat << EOF
hoge fuga
foo bar
fizz buzz
EOF
}
It "test"
The word 1 of line 3 of result of function myfunc should equal "fizz"
End
- Modifier は連鎖させることができる
-
myfunc
関数の実行結果の (result of function
) - 3行目の (
line 3 of
) - 1単語目 (
word 1 of
)
-
序数詞を使う
It "test"
When call echo Hello World
The second word of output should equal "World"
End
- Modifer に渡す引数が数値の場合は、序数詞を用いた表現に置き換えることができる
Matchers
It "test"
When call echo Hello World
The output should equal "Hello World"
End
- Matchers は、 Subject の内容を検証する方法を定義する要素になる
- 上記例でいうと、
equal
が該当する - いっぱい用意されている
否定
It "test"
When call echo hello
The output should not equal "hello"
End
-
should
の後ろにnot
をつければ、各 Matcher の否定を検証できる
satisfy
myfunc() {
test ${myfunc} = "hoge"
}
It "test"
When call echo hoge
The output should satisfy myfunc
End
-
satisfy <関数>
で指定した関数が 0 を返したら検証 OK になる - 検証対象の Subject は、
satisfy
に指定した関数と同じ名前の変数に格納されている
ファイル系の Matcher
Subjectで指定されたパスが条件を満たしているかどうかを検証する Matcher。
Matcher | 説明 |
---|---|
be exist |
ファイルやディレクトリが存在することを検証する1 |
be file |
ファイルであることを検証する |
be directory |
ディレクトリであることを検証する |
be empty file |
空のファイルであることを検証する |
be empty directory |
空のディレクトリであることを検証する |
be symlink |
シンボリックリンクであることを検証する |
be pipe |
パイプ2であることを検証する |
be socket |
ソケット3であることを検証する |
be readable |
読み取り可能であることを検証する |
be writable |
書き込み可能であることを検証する |
be executable |
実行可能であることを検証する |
be block_device |
ブロックデバイス4であることを検証する |
be character_device |
キャラクタデバイス4であることを検証する |
have setgid |
SGID5が設定されていることを検証する |
have setuid |
SUID6が設定されていることを検証する |
例
It "test"
The path ./test.sh should be exist
The path ./test.sh should be file
The path ./spec should be directory
End
終了ステータスの Matcher
Matcher | 説明 |
---|---|
be success |
ステータスが成功(0 )であることを検証する |
be failure |
ステータスが失敗(1 - 255 )であることを検証する |
例
myfunc() {
return 1
}
It "test"
When call myfunc
The status should be failure
End
文字列の Matcher
Matcher | 説明 |
---|---|
equal <STRING> / eq <STRING>
|
<STRING> と一致することを検証する |
start with <STRING> |
<STRING> で始まることを検証する |
end with <STRING> |
<STRING> で終わることを検証する |
include <STRING> |
<STRING> を含むことを検証する |
match pattern <PATTERN> |
<PATTERN> にマッチすることを検証する |
例
It "test"
When call echo Hoge
The output should eq "Hoge"
The output should start with "H"
The output should end with "e"
The output should include "og"
The output should match pattern "H*"
End
-
matche pattern
で使えるパターンの例-
*
: 任意の文字列 -
?
: 任意の一文字 -
[abc]
:[]
内のいずれかの文字-
[a-z]
のように、-
で範囲指定も可能 -
[!abc]
のように先頭に!
をつけることで否定(いずれでもない)にできる
-
-
abc|edf
: いずれかにマッチ
-
-
実装を見ると、内部的には
case
文のマッチが使われている
successful
myfunc() {
return 0
}
It "test"
The result of function myfunc should be successful
End
-
result of function
の結果が成功(0
)かどうかを検証する場合は、be successful
を使用する-
be success
だとエラーになる
-
変数の Matcher
Matcher | 説明 |
---|---|
be defined |
変数が定義されていることを検証する |
be undefined |
変数が定義されていないことを検証する |
be present |
変数に値が設定されている(空文字ではない)ことを検証する |
be blank |
変数に値が設定されていない(未定義か空文字である)ことを検証する |
be exported |
変数が export されていることを検証する |
be readonly |
変数が読み取り専用であることを検証する |
It "test"
HOGE=
PIYO=piyo
export FOO=foo
BAR=bar
readonly BAR
The variable HOGE should be defined
The variable FUGA should be undefined
The variable PIYO should be present
The variable HOGE should be blank
The variable Fuga should be blank
The variable FOO should be exported
The variable BAR should be readonly
End
他のシェルスクリプトを読み込む
test.sh
#!/bin/bash
myfunc() {
echo Hello World
}
test_spec.sh
Include ./test.sh
It "test"
When call myfunc
The output should equal "Hello World"
End
-
Include <読み込むシェルスクリプト>
で、指定したシェルスクリプトを現在のシェルで実行する-
source
している感じ
-
- テスト対象の関数が定義されているシェルスクリプトとかをこれで読み込んでテストする感じになる
テスト前後の処理
beforeAll1() {
echo beforeAll1
}
afterAll1() {
echo afterAll1
}
beforeEach1() {
echo beforeEach1
}
afterEach1() {
echo afterEach1
}
beforeAll2() {
echo beforeAll2
}
afterAll2() {
echo afterAll2
}
beforeEach2() {
echo beforeEach2
}
afterEach2() {
echo afterEach2
}
Describe "Describe1"
BeforeAll beforeAll1
AfterAll afterAll1
BeforeEach beforeEach1
AfterEach afterEach1
It "Test1"
echo "Test1"
End
It "Test2"
echo "Test2"
End
Describe "Describe2"
BeforeAll beforeAll2
AfterAll afterAll2
BeforeEach beforeEach2
AfterEach afterEach2
It "Test3"
echo "Test3"
End
End
End
実行結果(見やすいように整形)
beforeAll1
beforeEach1
Test1
afterEach1
beforeEach1
Test2
afterEach1
beforeAll2
beforeEach1
beforeEach2
Test3
afterEach2
afterEach1
afterAll2
afterAll1
-
BeforeAll
,AfterAll
に関数名を渡すことで、 example グループブロックの前後に処理を入れることができる -
BeforeEach
,AfterEach
に関数名を渡すことで、 example ブロックの前後に処理を入れることができる- 入れ子にした example グループブロック内の example ブロックにも適用される
- すべてのテスト仕様に共通で適用したい場合は、ヘルパーファイルで設定可能
- 詳細は こちら
標準入力のデータを定義する
myfunc() {
read ans
if [ "${ans}" = "y" ]; then
echo yes
else
echo no
fi
}
It "Test"
Data "y"
When call myfunc
The output should equal "yes"
End
-
When
の前にData <STRING>
をという形で、When
で実行する処理に流し込む標準入力の値を定義できる- 上の例では、
read
コマンドで読み取る値に対してy
という値を標準入力を流し込むようにしている
- 上の例では、
- 複数行の定義も可能
複数行の標準入力を定義する場合
It "Test"
Data
#|hoge
#|fuga
#|piyo
End
When call cat
The line 2 of output should equal "fuga"
End
-
Data
からEnd
までの間に、複数行の標準入力値を記載する -
#|
から後ろが実際に使用される値になる - 上記例の場合、値の中に変数を埋め込むことはできない
- 変数を埋め込む場合は以下のようにする
複数行入力の中に変数を埋め込みたい場合
It "Test"
MESSAGE="Hello World"
Data:expand
#|hoge
#|MESSAGE=${MESSAGE}
#|piyo
End
When call cat
The line 2 of output should equal "MESSAGE=Hello World"
End
-
Data:expand
で開始するように書くと、変数の埋め込みが可能になる - 他にも、関数の実行結果を渡すことも可能
関数の実行結果を標準入力にする
myfunc() {
echo "Hello $1"
}
It "Test"
Data myfunc world
When call cat
The output should equal "Hello world"
End
-
Data <関数> [arguments...]
で、関数の実行結果を標準入力として利用できる - ファイルの内容を入力することも可能
ファイルの内容を標準入力にする
echo "Hello World" > input.txt
It "Test"
Data < input.txt
When call cat
The output should equal "Hello World"
End
-
Data < <入力ファイル>
で、指定したファイルの内容を標準入力にできる
パラメータ化テスト
Parameters:value 1 2 "hoge"
It "test $1"
echo "test1 param=$1"
End
It "test $1"
echo "test2 param=$1"
End
Describe "Describe"
Parameters:value 3 4 "fuga"
It "test $1"
echo "test3 param=$1"
End
End
実行結果
test1 param=1
test1 param=2
test1 param=hoge
test2 param=1
test2 param=2
test2 param=hoge
test3 param=1
test3 param=2
test3 param=hoge
test3 param=3
test3 param=4
test3 param=fuga
Examples:
1) test 1
...
2) test 2
...
3) test hoge
...
4) test 1
...
5) test 2
...
6) test hoge
...
7) Describe test 1
...
8) Describe test 2
...
9) Describe test hoge
...
10) Describe test 3
...
11) Describe test 4
...
12) Describe test fuga
...
-
Parameters:value <params...>
で、テストの実行ごとに渡すパラメータを定義できる -
Parameters
を定義した example グループブロック内のすべての example ブロックに適用される - パラメータは、位置パラメータとして参照できる
-
It
に渡す説明の中でも参照できる
1回に複数のパラメータを渡す
Parameters
1 2 "hoge"
3 4 "fuga"
End
It "test"
echo "$#: $@"
End
実行結果
3: 1 2 hoge
3: 3 4 fuga
-
Parameters
~End
で、各テスト実行ごとに複数のパラメータを渡すように定義できる - 各行に、スペース区切りで渡すパラメータを記載する
組み合わせでパラメータを渡す
Parameters:matrix
child adult
male female "not known" "not applicable"
japanese foreigner
End
It "test"
echo "$@"
End
実行結果
child male japanese
child male foreigner
child female japanese
child female foreigner
child not known japanese
child not known foreigner
child not applicable japanese
child not applicable foreigner
adult male japanese
adult male foreigner
adult female japanese
adult female foreigner
adult not known japanese
adult not known foreigner
adult not applicable japanese
adult not applicable foreigner
-
Parameters:matrix
を使うと、列挙した値の組み合わせをパラメータにしてテストを実行できる
動的に定義する
Parameters:dynamic
for i in 1 2 3; do
%data "#$i" 1 2 3
done
End
It "test"
echo "$@"
End
実行結果
#1 1 2 3
#2 1 2 3
#3 1 2 3
-
Parameters:dynamic
を使うと、パラメータを動的に定義できる -
%data
は動的にパラメータを定義するときに使用する特殊なディレクティブで、この引数に渡した値がパラメータとして利用される
ディレクティブ
- シェルスクリプトだと書きにくいものとかを書きやすくするちょっとした組み込みの命令(ディレクティブ)が用意されている
- パッと見で使いそうな気がしたものだけピックアップ
- 全体は ドキュメント を参照
- ディレクティブはシェル関数ではないので、使用できる箇所に制限がある
- 関数定義の冒頭か、行の先頭でのみ使用できる
テキストの埋め込み
MESSAGE=$(
%text
#|hoge
#|fuga
#|piyo
)
It "test"
When call echo "$MESSAGE"
The line 2 of output should equal "fuga"
End
-
%text
ディレクティブを使うと、テキストの埋め込みができる-
%text
の下に埋め込むテキストを記述する -
#|
から後ろが埋め込まれるテキストになるので、インデントの影響を受けずに複数行のテキストが書ける
-
- ヒアドキュメントよりもきれいに複数行のテキストを記述できる
-
%text
は行の最初に存在する必要があるので、$(%text
とは書けない -
%text
は、実行時に以下のように変換される
%textの変換後の状態
MESSAGE=$(
shellspec_cat <<'DATA-DELIMITER-1667056487-735'
hoge
fuga
piyo
DATA-DELIMITER-1667056487-735
)
-
shellspec_cat
が具体的にどういう関数かはわからないが、おそらくcat
コマンド的なものだと思われる -
%text
ディレクティブはcat
を使ったヒアドキュメントに置き換えられる、と考えればどこでどういう書き方をすれば利用できるか、おのずとイメージできると思う
変数の保存
test.sh
#!/bin/sh
MESSAGE=hello
test_spec.sh
It "test"
When run source ./test.sh
The variable MESSAGE should equal "hello"
End
-
test.sh
を実行し、定義されたMESSAGE
変数の値を検証しようとしている
実行結果
Examples:
1) test
When run source ./test.sh
1.1) The variable MESSAGE should equal hello
expected: "hello"
got: <unset>
- テストは失敗する
-
When run source
を使っても、シェルスクリプトはサブシェルで実行されるっぽくて、宣言した変数などは呼び出し元では参照できない - これを、呼び出し元でも参照できるようにするために
%preserve
ディレクティブがある
変数を参照できるようにする
It "test"
preserve() {
%preserve MESSAGE
}
AfterRun preserve
When run source ./test.sh
The variable MESSAGE should equal "hello"
End
-
AfterRun
でWhen run
の実行後に処理を差し込んで、%preserve
ディレクティブで保存する変数名を指定する- 変数名は一度に複数指定可能 (
%preserve AAA BBB CCC
)
- 変数名は一度に複数指定可能 (
- これで、呼び出し元でも宣言された変数の値を参照できるようになる
ヘルパー
- デフォルトで生成される
spec/spec_helper.sh
には、すべてのテスト仕様で共通の設定や、グローバルな関数を定義することができる - 細かい話は公式ドキュメントを参照
グローバル関数を定義する
フォルダ構成
<PROJECT-ROOT>
|-.shellspec
`-spec/
|-spec_helper.sh
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
spec_helper.sh
# 他はデフォルト出力のまま
...
my_global_func() {
echo "global function from $1"
}
-
spec_helper.sh
に、独自関数(my_global_func
)を定義している
test1_spec.sh
my_global_func "test1"
test2_spec.sh
my_global_func "test2"
実行結果
$ shellspec
global function from test2
global function from test1
-
spec_helper.sh
に関数を定義すれば、その関数はすべてのテスト仕様から参照できる
全テスト仕様共通の前処理・後処理を登録する
フォルダ構成
<PROJECT-ROOT>
|-.shellspec
`-spec/
|-spec_helper.sh
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
spec_helper.sh
# 他はデフォルト出力のまま
...
spec_helper_configure() {
# Available functions: import, before_each, after_each, before_all, after_all
: import 'support/custom_matcher'
before_all "global_before_all"
after_all "global_after_all"
before_each "global_befor_each"
after_each "global_after_each"
}
global_before_all() {
echo "global before all"
}
global_after_all() {
echo "global after all"
}
global_befor_each() {
echo "global before each"
}
global_after_each() {
echo "global after each"
}
-
spec_helper_configure
関数の中で、全テスト仕様で共通に適用する前後処理を登録できる
登録用関数 | 説明 |
---|---|
before_all |
各テスト仕様のファイルが実行される前の処理を登録する |
after_all |
各テスト仕様のファイルが実行された後の処理を登録する |
before_each |
各 example ブロックが実行される前の処理を登録する |
after_each |
各 example ブロックが実行された後の処理を登録する |
test1_spec.sh
Describe "describe1"
It "test1-1"
echo "test1-1"
End
It "test1-2"
echo "test1-2"
End
Describe "describe1-1"
It "test1-1-1"
echo "test1-1-1"
End
End
End
test2_spec.sh
It "test2"
echo "test2"
End
実行結果(見やすいように整形)
global before all
global before each
test2
global after each
global after all
global before all
global before each
test1-1
global after each
global before each
test1-2
global after each
global before each
test1-1-1
global after each
global after all
ヘルパーファイルの場所を変更する
フォルダ構成
<PROJECT-ROOT>
|-.shellspec
|-shellspec/
| `-spec_helper.sh
`-spec/
|-test1_spec.sh
`-subdir/
`-test2_spec.sh
-
shellspec
ディレクトリを作成し、その下にヘルパーファイルを配置
spec_helper.sh
# 他はデフォルト出力のまま
...
my_global_func() {
echo "my global fun"
}
test1_spec.sh
my_global_func
test2_spec.sh
my_global_func
実行結果
$ shellspec --helperdir shellspec
my global fun
my global fun
-
--helperdir
で、ヘルパーファイルが配置されているディレクトリを指定する
モック
Describe "d1"
Describe "d2"
# pwd コマンドのモックを定義
pwd() {
echo "mocked pwd"
}
It "test1"
When call pwd
# モック化された pwd が呼ばれる
The output should equal "mocked pwd"
End
End
It "test2"
# こっちは pwd のモックを定義していないので、従来の pwd コマンドが実行される
When call pwd
The output should equal "/work/hello-world"
End
End
- スコープの中でモック化したいコマンドやシェル関数と同名の関数を定義することで、そのサブシェルの中ではモック関数が代わりに呼ばれるようなる
- スコープを出たら、本来のコマンドやシェル関数の動きに戻る
- この方法は関数ベースのモック(function-based mock)と呼び、基本的にはこの方法が推奨される
- 関数名として利用できない文字が含まれているコマンドをモックしたいときなどは、もう1つのモック化の方法であるコマンドベースのモック(command-based mock)を使用する(後述)
- ハイフンは関数名に使用できないので、例えば
docker-compose
とかをモック化したいならコマンドベースのモックが必要になる
シェルスクリプト内で使用されているコマンドをモックにする
さきに、うまくいかないケース。
test.sh
#!/bin/sh
pwd
test_spec.sh
pwd() {
echo "mocked pwd"
}
It "test"
When run ./test.sh
The output should equal "mocked pwd"
End
実行結果
Running: /bin/sh [sh]
F
Examples:
1) test
When run ./test.sh
1.1) The output should equal mocked pwd
expected: "mocked pwd"
got: "/work/hello-world"
# spec/test_spec.sh:7
-
When run
でシェルスクリプトを実行した場合、シェルスクリプト内で参照されているコマンドはモックには差し替えられない - この場合は、
When run source
でシェルスクリプトを実行することでモックが有効になる
test_spec.sh
pwd() {
echo "mocked pwd"
}
It "test"
When run source ./test.sh
The output should equal "mocked pwd"
End
実行結果
Running: /bin/sh [sh]
.
Finished in 0.03 seconds (user 0.02 seconds, sys 0.00 seconds)
1 example, 0 failures
- モック化できている
コマンドベースのモック
Mock docker-compose
echo "mocked docker-compose"
End
It "test"
When run docker-compose
The output should equal "mocked docker-compose"
End
-
Mock
を使用することで定義できる-
Mock <モック化するコマンドの名前>
と指定する -
Mock
からEnd
の間に、モックの処理を記述する
-
- 関数ベースのモックに比べて良い点は以下
- 関数名に使用できない文字(
-
など)を含むコマンドもモックとして定義できる - 外部コマンドからもモックのコマンドが実行されるようになる(らしい。試してはいない)
- 関数名に使用できない文字(
- シェル関数やシェル組み込みの関数はモック化できない
-
help
コマンドで出てくる奴ら(echo
とかif
とか)
-
- そのほかにもいくつか制約があるので、詳細はドキュメントを参照
カバレッジ
- kcov/src at master · SimonKagstrom/kcov · GitHub を使ったカバレッジの取得ができる
- 事前に kcov をインストールしておく必要がある
kcovのインストール
$ apt-get install -y kcov
test.sh
#!/bin/sh
if [ "$1" = "y" ]; then
echo "yes"
else
echo "no"
fi
test_spec.sh
It "test"
When run script ./test.sh "y"
The output should equal "yes"
End
実行結果
$ shellspec -s bash --kcov
Running: /usr/bin/bash [bash 5.2.2(1)-release]
.
Finished in 0.25 seconds (user 0.10 seconds, sys 0.01 seconds)
1 example, 0 failures
Code covered: 66.67%, Executed lines: 2, Instrumented lines: 3
- カバレッジ取得を有効にするには、
--kcov
オプションを指定する - カバレッジを取得できるのは、 bash, zsh, ksh のいずれかに限られるので、
-s bash
で bash を使うようにしている - 収集されたカバレッジの情報は
coverage
ディレクトリの下に出力される - ↓のように、HTML形式で出力されている
- 同じディレクトリに json や xml でも出力されている
カバレッジ計測の対象
デフォルトでは、以下のコードがカバレッジ計測の対象となる。
-
Include
で読み込んだシェルスクリプト -
When
で評価され実行されたシェル関数 -
When run script
で評価され実行されたシェルスクリプト -
When run source
で評価され実行されたシェルスクリプト
カバレッジレポートの対象
- デフォルトでは、 shellspec のプロジェクトディレクトリ配下に存在する
.sh
で終わるファイルがレポートの対象となる
フォルダ構成
<PROJECT-ROOT>
|-.shellspec
|-spec/
| |-spec_helper.sh
| `-test_spec.sh
|-src/
| |-foo.sh
| |-bar.sh
| `-hoge
`-fuga.sh
foo.sh
#!/bin/sh
echo "foo"
bar.sh
#!/bin/sh
echo "bar"
hoge
#!/bin/sh
echo "hoge"
fuga.sh
#!/bin/sh
echo "fuga"
test_spec.sh
It "test1"
When run script ./src/foo.sh
The output should equal "foo"
End
It "test2"
PATH=${PATH}:${SHELLSPEC_PROJECT_ROOT}/src
When run command bar.sh
The output should equal "bar"
End
-
src/foo.sh
は、When run script
で実行してテストしている -
src/bar.sh
は、When run command
で実行してテストしている
実行結果
$ shellspec -s bash --kcov
Running: /usr/bin/bash [bash 5.2.2(1)-release]
..
Finished in 0.35 seconds (user 0.10 seconds, sys 0.04 seconds)
2 examples, 0 failures
Code covered: 33.33%, Executed lines: 1, Instrumented lines: 3
カバレッジレポート
- カバレッジレポートの対象となっているのは以下の3ファイル
fuga.sh
src/foo.sh
src/bar.sh
-
src/hoge
は、拡張子.sh
で終わらないのでレポートの対象に入っていない - 一方、カバレッジが計測されているのは
src/foo.sh
のみとなっている-
src/bar.sh
はWhen run command
で実行したため、カバレッジ計測の対象外となっている
-
カバレッジのレポート対象を変更する
$ shellspec -s bash --kcov --kcov-options "--include-path=src --include-pattern="
Running: /usr/bin/bash [bash 5.2.2(1)-release]
..
Finished in 0.32 seconds (user 0.08 seconds, sys 0.05 seconds)
2 examples, 0 failures
Code covered: 33.33%, Executed lines: 1, Instrumented lines: 3
-
--kcov-options
で、 kcov のオプションを指定する -
--include-path
で、レポート対象のディレクトリを指定し、--include-pattern
で対象のファイルパターンを指定する- 空にしたら全ファイルが対象になるっぽい(よくわかってない)
- デフォルトは
--include-path=.
,--include-pattern=.sh
が設定されているので、これを書き換える必要がある - デフォルト値は自動生成される
.shellspec
に書かれている
-
fuga.sh
がレポート対象から外れ、src/hoge
がレポート対象に追加されている
特別な環境変数
プロジェクトディレクトリなど、いくつかのメタ情報的なものを環境変数から参照できるようになっている。
全容は以下に記載されている。
Special environment Variables | shellspec/references.md at master · shellspec/shellspec · GitHub
ここでは、比較的よく使いそうな気がするものだけピックアップ。
変数名 | 説明 |
---|---|
SHELLSPEC_PROJECT_ROOT |
プロジェクトルート |
SHELLSPEC_SPECFILE |
現在実行中のテスト仕様ファイルのパス |
SHELLSPEC_SPEC_NO |
現在実行中のテスト仕様ファイルの番号 |
SHELLSPEC_GROUP_ID |
現在の example グループブロックの ID (例: 1-2 ) |
SHELLSPEC_EXAMPLE_ID |
現在の example ブロックの ID (例: 1-2-3 ) |
SHELLSPEC_EXAMPLE_NO |
現在の example ブロックの連番 |
参考
-
ドキュメントでは非推奨扱いになっていて
exist
を使うように書かれているけど、実際にexist
を使うとそんなもんはねぇって怒られるのでbe exist
を使うしかない(0.28.1 で確認) ↩