8
10

More than 1 year has passed since last update.

ShellSpec使い方メモ

Posted at

ShellSpecとは

環境

  • 以下のコマンドで準備した 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.shspec ディレクトリの下に置いたままにしている
    • 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 であるファイルを指定している
    • ここで、 --patterntest*.sh と指定してもうまくいかない
    • --pattern で指定するパターンは、 --default-path からの相対パスではなく、プロジェクトルートからの相対パスに対してマッチングする
    • 例えば、 test-module1.shmodule1/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 ブロックを含めることができる
      • 要するに DescribeIt を入れ子にできる、ということ
    • DescribeContext は、 ExampleGroup のエイリアス
  • 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 の行が該当する

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 ...
    • シェルスクリプトを実行する
    • シバンは無視される
    • 呼び出し元で宣言した変数などが引き継がれた状態で実行される
      • ドキュメントには、「Can be refer to variables inside the shell script.(シェルスクリプト内の変数を参照できる)」と書かれている
      • しかし、実際に試すと呼び出したシェルスクリプト内で設定した変数の値は呼び出し元からは参照できない(バグ?)
    • モックを利用したいときはこれで実行する必要がある(詳細はこちら

まとめると、以下のような感じ。

比較項目 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
  • ParametersEnd で、各テスト実行ごとに複数のパラメータを渡すように定義できる
  • 各行に、スペース区切りで渡すパラメータを記載する

組み合わせでパラメータを渡す

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
  • AfterRunWhen 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のインストール
$ 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 でも出力されている

image.png

image.png

カバレッジ計測の対象

デフォルトでは、以下のコードがカバレッジ計測の対象となる。

  • 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

カバレッジレポート

image.png

  • カバレッジレポートの対象となっているのは以下の3ファイル
    • fuga.sh
    • src/foo.sh
    • src/bar.sh
  • src/hoge は、拡張子 .sh で終わらないのでレポートの対象に入っていない
  • 一方、カバレッジが計測されているのは src/foo.sh のみとなっている
    • src/bar.shWhen 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 に書かれている

image.png

  • 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 ブロックの連番

参考

  1. ドキュメントでは非推奨扱いになっていて exist を使うように書かれているけど、実際に exist を使うとそんなもんはねぇって怒られるので be exist を使うしかない(0.28.1 で確認)

  2. 「名前付きパイプ」(FIFO/Named Pipe)を使ってプロセス間通信を試してみる | ゲンゾウ用ポストイット

  3. Linuxのファイルの種類 - Qiita

  4. デバイスファイル - Wikipedia 2

  5. SGID(Set Group ID) - 特殊なアクセス権

  6. SUID(Set User ID) - 特殊なアクセス権

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10