シェルスクリプトのテスト、何を使ってる？shUnit2？Bats？ ShellSpec を使ってみませんか？

# ShellSpec - BDD style testing framework の紹介

まだ [shUnit2](https://github.com/kward/shunit2) や [Bats](https://github.com/sstephenson/bats) (or [Bats-core](https://github.com/bats-core/bats-core)) で消耗してるの？なんてタイトルで釣ろうかと思いましたが、おとなしめのタイトルにしました。（笑）

[前回](https://qiita.com/ko1nksm/items/77388d75b8c1f18c0058)は [ShellSpec](https://shellspec.info) の使い方は [README.md](https://github.com/ko1nksm/shellspec/blob/master/README.md) にまかせて開発したときの思いの丈（？）を書いたのですが、先日 ShellSpec をアップデートし大きく機能強化したのを機に、今回は使い方と新機能を紹介したいと思います。

 (2019/08/23追記) 0.20.0 までの機能を追記しました。
 (2020/09/17追記) この記事を補完・更新する記事として以下の記事を書きました 。
[ShellSpec - シェルスクリプト用のフル機能のBDDユニットテストフレームワーク](https://qiita.com/ko1nksm/items/2f01ff4f50e957ebf1de)


## ShellSpec とは

ShellSpec は BDD スタイルのテスティングフレームワークです。他の BDD スタイルのテスティングフレームワークと同様に、自然言語に近い DSL で構造化されたテスト（実行可能な例）を記述することができます。

* 公式ウェブサイト https://shellspec.info
* github プロジェクトページ https://github.com/ko1nksm/shellspec

POSIX 互換のシェルスクリプトで実装されており、依存している外部コマンドも POSIX 準拠の基本的なものだけなので bash だけでなく dash や zsh や ksh などより多くのシェルで動作します。OS も様々なものに対応しており、Debian, macOS, Soralis, Windows（WSL）や Docker 上の Alpine Linux や組み込みで使われることが多い BusyBox でもそのまま動きます。

[RSpec](http://rspec.info/) に大きく影響を受けており、似ている機能や設計でありながら、シェルスクリプトの用途に適した DSL となっています。

### スペックファイルについて

以下は ShellSpec の最小のスペックファイルの例です。

```sh
Example
  The value 123 should equal 123
End
```

「123 は 123である」というあまり意味がない例ですが、シェルスクリプトを知らなくとも自然言語（英語ですが・・・）として読むことが出来ます。

もう少し意味があるスペックファイルの例です。

```sh
#shellcheck shell=sh

Describe 'sample'
  Describe 'calc()'
    calc() { echo "$(($*))"; }

    It 'calculates the formula'
      When call calc 1 + 2
      The output should equal 3
    End
  End
End
```

calc 関数が計算式を計算できることを確認する例です。ネスト可能な `Describe` でテストを構造的にグループ化し、 `It` でテストの説明を行い、 `When` ～ で関数を呼び出し、`The` ～ でその出力をチェックしています。

最初の例では `Example` を使用しましたが、今回は `Example` のエイリアスである `It` を使用し、*It calculates the formula* と読めるようにすることで可読性を上げています。

ちなみに旧バージョンでは `It` は `Example` のエイリアスではなかったのですが、ShellSpec 自身のスペックファイルを読みやすく書き直しているうちに、やはりこの方法が読みやすいと思い直し、他のツールと同様の方法に変更しました。

今回はスペックファイル自体に calc 関数を定義していますが、もちろん外部ファイルに記述して `Include` で読み込むことも可能です。通常はその方法が主な使い方となるでしょう。スペックファイルに直接書いてよいのは、DSL と関数定義のみです。もちろん関数の中は自由に書いてよいですが、関数の外に実行コードは書かないようにしてください。（動作に支障があるということではなくテストの独立性を保つための方針です）

1行目の `#shellcheck shell=sh` は ShellSpec ではなく [ShellCheck](https://www.shellcheck.net) という lint ツールの命令で、このファイルを（bash等ではなく）sh として解釈させるためのものです。すなわちそれは、このスペックファイルがシェルスクリプトの文法として解釈できるということを意味しています。

大文字で始まる DSL（`Describe` や `It` や `End` のことです）があったりインデントでブロックが表現されていたりしますが、それでもスペックファイルはシェルスクリプトの文法として正しいものとなります。そのためスペックファイルに対して `shellcheck` や `sh -n` を使用した構文チェックなど既存のツールを使用することが出来ます。

しかしながらスペックファイルが直接シェル上で動くわけではありません。ShellSpec が実行時に動的にコードを変換しています。DSL は ShellSpec の内部関数の呼び出しに変換され、ブロックはサブシェルに変換されます。（変換が行われるのはスペックファイルのみです。外部スクリプトは変換されません）

ブロックをサブシェルに変換することで各テストはそれぞれ独立した環境で実行されます。これによりレキシカルスコープに似たスコープを実現しています。各テストをサブシェルで実行することでテストの独立性を保ち、ローカル変数や関数の一時的な再定義（モック・スタブ）も行えるようになります。（ちなみにシェルスクリプトの `local` や `typeset` は、POSIX準拠ではなくダイナミックスコープなので使用していません。）

### レポート機能

最近のテストツール同様、モダンな表示を行います。ShellSpec を実行すると、成功したテストと失敗したテストがそれぞれ表示され、失敗したテストは行番号とともに失敗した理由が表示されます。（shUnit2 では行番号の表示に `${_ASSERT_EQUALS_}` を使用した特別な書き方と、`$LINENO` 変数に対応したシェルが必要ですが、ShellSpec はすべてのシェルで行番号の表示が行なえます！Debian や Ubuntu の dash は パフォーマンスを理由に `$LINENO` 変数は無効にされているのです・・・）

複数の出力形式に対応しており、デフォルトのドットによる表示 (progress) の他、ドキュメントとして読みやすい documentation や tap 形式に対応しています。

**progress formatter (default)**

![image.png](https://qiita-image-store.s3.amazonaws.com/0/54918/12c9a964-445f-7eaa-222b-45100c77e890.png)

**documentation formatter**

![image.png](https://qiita-image-store.s3.amazonaws.com/0/54918/6bdc268c-af73-88e0-f6fb-820e421b2960.png)

**tap formatter**

![image.png](https://qiita-image-store.s3.amazonaws.com/0/54918/9a9db191-48d6-d47c-9ce7-ef9513611a12.png)

**junit formatter**

（省略）

### フィルタリング (2019/08/23追記)

テスト実行の際に、行番号、ID、フォーカス、テスト名、タグ、といった様々な方法で実行するテストを指定することが出来ます。一番使用頻度が多いと思われる行番号は、（RSpecのように）以下の方法で指定することが出来ます。

```
shellspec path/to/a_spec.sh:10:20     # 10行目と20行目を含むテストを実行
```

またテストの、`Descript` や `It` をそれぞれ `fDescribe`、`fIt` と書き換えることでフォーカスを指定し、フォーカスされたテストだけを実行することが出来ます。これも RSpec を参考に実装したのですが、RSpecとは違い実行するときに `--focus` オプションが必要です。（指定しない場合はすべて実行されます）

これは ShellSpec の設計上 1 パス、つまりスペックファイルを変換しながらそのまま実行しているため、最初にスペックファイルにフォーカスされた行があるか知ることが不可能だからです。`--focus` オプションが必要なのは制限とも思えるのですが実際のテスト修正では、エラーとなった行を修正 → フォーカスしたものだけを実行 → 全体を実行して確認 → エラーが修正されていなければその箇所のみ修正 → 繰り返す と フォーカスしたものと全体の実行を交互に行うと思うので、これはこれで良かったかなぁと思っています。


### 並列実行 (2019/08/23追記)

バージョン 0.10.0 で並列実行に対応しました。並列実行は Bats-core でも対応予定（おそらく1.2.0から。master には含まれています）ですが、 Bats-core では GNU parallel が必要なのに対して ShellSpec はシェルスクリプトのバックグラウンドプロセスを利用しシェルスクリプトのみで実装しています。そのためすべてのシェルで利用することが出来ます。

### ランダム実行 (2019/08/23追記)

バージョン 0.14.0 でスペックファイルのランダム実行に対応しました。これもシェルスクリプト（と`sort`と`od`コマンド）のみで実行しています。実行順のランダム化には `fnv1a` で（`od` コマンドで 8 進数に変換した）ファイル名をハッシュ化し `xorshift32` でハッシュ値からランダム値を求めてsortしています。

ハッシュを求めるコマンドは `cksum` （や `sha1sum`、`md5sum`等）を使用することも出来たのですが、これだとファイルの数だけコマンド呼び出しが発生するので遅くなります。また `$RANDOM` や `/dev/urandom` でランダム値を取得することも可能ですが、シェルや OS に依存します。そのためハッシュ化とランダム値の計算をシェルスクリプトで実装しました。どちらも軽量なアルゴリズムのためシェルスクリプトで実装しても特に遅さは感じません。

### カバレッジ統合 (2019/08/23追記)

バージョン 0.16.0 で[kcov](http://simonkagstrom.github.io/kcov/index.html) を統合させることにより、カバレッジ機能に対応させました。もちろんカバレッジレポートも出力できるのでテストを行っていない箇所を可視化することが出来ます。

どのテスティングフレームワークにも言えることですがカバレッジ機能がフレームワークに統合されていない場合、それを自分で統合させるのはけっこう大変な作業です。ShellSpec では設計上「Kcov 上で ShellSpecを実行」するだけではカバレッジを取ることが出来ないため、ShellSpec に統合させました。

なおカバレッジを取ることが出来るシェルは ~~bash のみです。（Kcov は bash のみに対応しており、Kcov は bash のデバッグ機能に依存しています。）これが今の所唯一の特定のシェルでしか使えない機能となります。~~ bash, zsh, ksh です。Kcov は bash しか対応していませんが、bash と同様の出力を行うことで、zsh と ksh に対応させました。（2020/05/11）

### プロファイラ対応 (2019/08/23追記)

バージョン 0.18.0 でプロファイラに対応しています。時間がかかっているテストをリストアップすることができます。が、はっきり言ってこれは一番意味がない機能だと思っています（笑）

アプリケーションのプロファイラならともかく、テストのプロファイラにどういう意味があるのでしょうか？時間がかかっているテストだから短くしようという意味がなくはないですが、テストで時間がかかっているからと言って、実際のアプリケーションではボトルネックになっていなかったり、逆にテストでは短い時間でも実際のアプリケーションでは何度も呼び出すためボトルネックになるということがあるのでテストでプロファイラを使用してもあまり意味がないと思っています。

ではなぜ実装したのか？ですが、jUnit XML で（必須ではないですが）テスト実行時間の項目があるからです。というのは建前で、面白い実装方法を思いつたからです。

多くの環境をサポートしようとするとシェルスクリプトでミリ秒の取得ができないという制限にぶち当たります。`date` コマンドは POSIX の範囲ではミリ秒の取得ができません。POSIX の範囲に限定するとシェルスクリプトでミリ秒を取得できるコマンドは `time` と `times` のみです。このうち `times` は user 時間と sys 時間は取得できますが肝心の real 時間が取得できません。そのため使えるコマンドは `time` のみです。しかし `time` は呼び出したコマンドの実行時間を調べるものなので、テスト一つごとに外部プロセスとして呼び出さなければいけません。テストでシェル関数呼び出しなどを行うのでそれは不可能です。

そこでとった手段が、プロファイリングのために数値をカウントするだけのプロセスをバックグラウンド実行させるという方法です。テスト全体の実行時間は `time` でわかるので、あとはテスト 1 件の開始と終了のタイミング（＝カウント）がわかれば計算できます。テストの開始と終了の時点で、プロファイラプロセスに通知してその時のカウント値を記録させています。（最初はシグナルを用いて通知していましたが扱いが大変だったのでファイルを使用しています。）

つまり CPU コア一つを全力でぶん回すというかなり強引な手段です。テストの実行時間に影響があるのでは？とか精度が悪いのでは？という懸念はありましたがそれなりに上手く言っているようです。（あまり意味がない機能だし・・・）

### シングルスクリプトファイルテスト (2019/08/23追記)

バージョン 0.19.0でシングルスクリプトファイルのテストに対応しました。多くのシェルスクリプトは1ファイルの実行可能なスクリプトとして作成されていることが多いと思います。関数だけで定義されているライブラリファイルを読み込む場合は ShellSpec の `Include` で簡単に読み込んでテストできるのですが、実行可能なスクリプトファイルになっている場合テストが困難でした。

バージョン 0.19.0で対応した機能により、シェルスクリプトに `Interception point` を入れておくことでその場所で処理を割り込ませたり、特定の行でスクリプトを中断させる（`Sourced Return`）ことで、実行可能なスクリプトファイルのテストを可能にしました。

### パラメーター化テスト (2019/08/23追記)

バージョン 0.20.0 でパラメータ化テストに対応しました。jUnit5 や rspec-parameterized にあるような機能でテストに対してパラメータを定義することで同様のテストをパラメータを変えて実行する機能です。

実は当初の脳内設計には含まれていなくて、あとから追加したものなのですが、既存のテストのパラメータを定義するだけで簡単にパラメータ化テストに変更できるというシンプルな使い勝手を実現できたのでかなり気に入っています。（DSL はページの下の方を参照）

## 使い方

### インストール

github からコードを clone し、PATHが通った場所にシンボリックリンクを作成するだけです。

```sh
$ cd /SOME/WHERE/TO/INSTALL
$ git clone https://github.com/ko1nksm/shellspec.git
$ ln -s /SOME/WHERE/TO/INSTALL/shellspec/shellspec /EXECUTABLE/PATH/
# (e.g. /EXECUTABLE/PATH/ = /usr/local/bin/, $HOME/bin/)
```

もしくは tar.gz をダウンロードして展開しても良いです。

```sh
$ cd /SOME/WHERE/TO/INSTALL
$ wget https://github.com/ko1nksm/shellspec/archive/0.8.0.tar.gz
$ tar xzvf shellspec-0.8.0.tar.gz

$ ln -s /SOME/WHERE/TO/INSTALL/shellspec-0.8.0/shellspec /EXECUTABLE/PATH/
# (e.g. /EXECUTABLE/PATH/ = /usr/local/bin/, $HOME/bin/)
```

### はじめの一歩

通常はテスト対象のプロジェクトディレクトリがあると思うので、そのディレクトリ（なければ新規に作成してください）で `shellspec --init` を実行してください。スペックファイルを格納する `spec` ディレクトリと設定用のファイルが作成されます。

```sh
$ cd /YOUR/PROJECT/DIRECTORY
$ shellspec --init
  create .shellspec
  create spec/spec_helper.sh
```

（とは言ってもこれらのファイルは必須ではなく、適当な場所にスペックファイルを作成して、`shellspec` に渡せば実行できるのですが・・・）

あとは、`spec` ディレクトリの中にスペックファイルを作成していき、プロジェクトディレクトリ直下で `shellspec` を実行すれば、スペックファイルが実行されます。（スペックファイルの名前の末尾は `_spec.sh` である必要があります。）

## DSL

ShellSpec の DSL は大きく、*Example group*、*Example*、*Evaluation*、*Expectation* に分かれています。これらにあてはまらないものとして *Helper* と *Directive* があります。

```sh
Describe 'calc()'              # Example group
  calc() { echo "$(($*))"; }

  It 'calculates the formula'  # Example
    When call calc 1 + 2       # Evaluation
    The output should equal 3  # Expectation
  End
End
```

### Example group

BDD では一つのテストのことを実行可能な例（*Example*) と呼び、ShellSpec もそれに倣っています。

*Example group* は *Example* をグループ化するためのブロック構文です。ブロック構文とは特定のキーワードで始まり
`End`で終わる構文です。これは ShellSpec DSL の拡張構文で純粋なシェルスクリプトの構文ではありません。（シェルスクリプトとしてみると開始と終わりはそれぞれただの 1 命令として扱われます。）

*Example group* のDSLには `Describe` と `Context` があり、機能的にはどちらも同じですが、テスト対象についての説明を行う時に `Describe` を使用し、テストの状況を説明するときには `Context` を使用します。これらはネスト可能で、テスト対象や状況に応じて構造化してグループ化することが出来ます。

### Example

実行可能な例 (*Example*) です。このブロックの中に、何をして (*Evaluation*) どうあるべきか？(*Expectation*) を記述します。

*Example group* の DSL には ブロック構文の `Example`、`Specify`、`It`、ワンライナー構文の `Todo` があります。`It` を使って書くと説明をより自然な文章にすることが出来ますが、しっくりこない場合には、同等の機能である `Example` や `Specify` を使用することができます。`Todo` は実装予定の例を一行で記述するための DSLで、内容がない `Example` と同等の意味になります。

#### Evaluation

*Example* の中の `When` で始まる行が *Evaluation* です。*Evaluation* でテスト対象のシェル関数や外部コマンドを呼び出します。*Evaluation* は一つの *Example* の中に一つしか書けません。（省略することは出来ます。）

```sh
When call calc 1 + 2
```

`When` の次の `call` はシェル関数もしくは外部コマンドを呼び出すことを意味し、他に外部コマンドのみを呼び出す `run` やサブシェル内で呼び出す `invoke` （特殊な用途用なのであまり使いません）があります。一般的には `call` を使用すれば十分でしょう。

#### Expectation

*Example* の中の `The` で始まる行が *Expectation* です。*Evaluation* の実行結果を検証します。*Expectation* は複数書くことが出来ます。複数書いた場合でも途中の失敗で停止することなくすべての *Expectation* が実行されます。（RSpec の `aggregate_failures` 相当の動作です。）

*Expectation* は以下のような構成になっています。

```
The output should equal 3
 |    |           |
 |    |           +-- equal が matcher です。この場合は3であることを検証します。
 |    +-- output が subject （検証対象） です。この場合は標準出力を検証します。
 +-- Expectation は The で始まります。
```

`should` の代わりに `should not` を使用することで、否定の意味にすることも出来ます。

標準出力の2行目を検証したい等といった場合に使用するのが *modifier* です。 *modifier* は *subject* の内容を加工し新たな *subject* とします。

```
The line 2 of output should equal 3
    |
    +-- line が modifier です。この場合は標準出力の2行目を新たな subject とします。
```

*modifier* はつなげて書くことも出来ます。また数値は序数で書くことも出来ます。

```
The word 1 of line 2 of output should equal 3
The first word of second line of output should equal 3
```

*subject*、*modifier*、*matcher* には、標準エラー出力や終了ステータスや検証するための *subject* やファイルの状態や文字列が特定のパターンにマッチしているかを検証する *matcher* など、他にも色々あります。詳細はプロジェクトサイトを参照してください。

### Helper

#### Include, Before, After

以下は、`Include`、`Before`、`After` を使用したサンプルです。
（mylib.sh の中にはデータファイルの件数を数える count 関数と合計を求める sum 関数があると考えてください）

```sh
Describe "mylib.sh"
  Include ./mylib.sh
  Before "setup"
  After "cleanup"

  setup() {
    {
      echo 10
      echo 20
      echo 30
    } > "$SHELLSPEC_TMPBASE/data.txt"
  }
  cleanup() {
    rm "$SHELLSPEC_TMPBASE/data.txt"
  }

  Describe 'count()'
    It 'counts data file'
      When call count "$SHELLSPEC_TMPBASE/data.txt"
      The output should equal 3
    End
  End

  Describe 'sum()'
    It 'sums data file'
      When call sum "$SHELLSPEC_TMPBASE/data.txt"
      The output should equal 60
    End
  End
End
```

`Before`、`After` はそれぞれ、各 *Example* 実行の前後に呼び出されるフック関数（またはコード）を指定できます。初期化処理や終了処理を書く時に使用します。

`Before`、`After` で指定した関数が実行されるのは *Example* 毎であることに注意してください。つまりこの例の場合、`setup` 関数と `cleanup` 関数はそれぞれ2回ずつ呼び出されます。

`Include` も内部的に`Before` 相当のコードになるため、*Example* 毎に読み込まれます。（2019/03/28 追記 関数の再定義を直感的に記述できないため 0.9.0 でその場で読み込むように仕様変更します、）

スペック全体や `Describe` 単位で呼び出されるいわゆる `BeforeAll` や `BeforeContext` に相当する機能は意図的に実装していません。テストの独立性を壊す可能性があるためです。（とはいえ、スペックファイルはシェルスクリプトとして実行されるので実行したい箇所にコードを書けば普通に動くわけですが・・・。推奨はしません。）

#### Skip, Skip if, Pending

`Skip` を使用することで、現在のブロックの `Skip` 以降の処理をスキップすることが出来ます。`Skip if` は条件付きスキップで特定の環境でのみ *Example* をスキップしたいというときに使用します。

また `Describe`、`Context`、`Example`、`Specify`、`It` のそれぞれの頭に`x` をつけて`xDescribe`、`xContext`、`xExample`、`xSpecify`、`xIt` とすることでブロック自体を簡単にスキップすることもできます。

`Pending` は `Skip` と似ていますが機能が未実装であることを示す時に使用します。`Pending` は *Example* の実行を単にスキップする `Skip` とは違い *Example* を実行します。そして検証結果が失敗（未実装なので想定通り）であれば、スペック自体は成功となり、実装が完了し検証結果が成功となれば、スペックは失敗となり `Pending` を取り除くべきということがわかります。

#### Data

ShellSpec 0.8.0 からの新機能です。シェルスクリプトではしばしばデータの入力を標準入力から受け取るのですがそれを簡潔に書くことができる機能です。`Data` を使用すると以下のように標準入力からのデータ入力を記述することが出来ます。

```
Describe "sort command"
  Data
    #|2
    #|3
    #|1
  End

  It 'sorts data'
    When call sort
    The line 1 of output should equal 1
    The line 2 of output should equal 2
    The line 3 of output should equal 3
  End
End
```

`Data` ブロックの中のコメントの行頭から `#|` を除いた残りの部分が標準入力データになります。`Data:expand` とすることで変数展開を行ったり、関数の出力結果を標準入力データとしたり `Data | <FILTER>` と書くことで別のフィルタを間に入れるといったことも可能です。

### Directive

これも ShellSpec 0.8.0 からの機能です。他の DSL とは少し違う扱いなので *Directive* という別の名前で呼んでいます。

これらを使用して先程の Before, After のサンプルコードを書き直すと以下のようになります。

```sh
% DATA_FILE: "$SHELLSPEC_TMPBASE/data.txt"

Describe "mylib.sh"
  Include ./mylib.sh
  Before "setup"
  After "cleanup"

  setup() {
    %text > "$DATA_FILE"
    #|10
    #|20
    #|30
  }
  cleanup() { rm "$DATA_FILE"; }

  Describe 'count()'
    It 'counts data file'
      When call count "$DATA_FILE"
      The output should equal 3
    End
  End

  Describe 'sum()'
    It 'sums data file'
      When call sum "$DATA_FILE"
      The output should equal 60
    End
  End
End
```

`%` は `%const` の別名で定数を定義します。ただしこの定数の値はスペックファイルの変換時に決定されます。つまり ShellSpec の環境変数である `$SHELLSPEC_TMPBASE` などを参照することは出来ますが、スペックファイルの変数や関数は参照できません。

もともとこの機能は、条件付きスキップで必要になりました。通常は *Example* を実行の起点とし、`Before` 等で変数の初期化などを行うのですが、 `Skip if` は、*Example* 外で処理が実行されるため、まだ変数が初期化されていないわけです。そのため内部で作成する変数ではなく外部から与えられる定数という扱いで、`Describe` の外でファイルレベルの定数を定義できるようにしました。これは定数であるため名前は大文字限定としています。（これは ShellCheck の警告を抑制する効果もあります。）今はただの変数として実装されているためスペックファイル内で値の変更ができるのですが将来的には readonly にするかもしれません。

そしてもう一つの *Directive* が 埋め込みテキストである `%text` です。この *Directive* は他の DSL と違って関数の中で使用します。`%text` はシェルスクリプトのヒアドキュメントの代わりとなる機能を提供します。先程の `setup` 関数をヒアドキュメントを使用して書くと以下のようになります。

```sh
  setup() {
    cat < 'HERE' > "$DATA_FILE"
10
20
30
HERE
  }
```

シェルスクリプトのヒアドキュメントはインデントできないため、インデントされたコードで非常に見づらいものとなってしまいます。（正確にはハードタブを使用すればインデントは可能なのですが、ヒアドキュメントの終わりはインデントできず、ぱっと見でタブとスペースの区別がつかないためいろいろと問題があります。）

先程の例では `%text` の内容をリダイレクトでファイルに出力しましたが、リダイレクトを書かなければ標準出力に出力されます。`Data` ブロックと同様、`%text:expand` で変数展開を行ったり別のフィルタと組み合わせることも出来ます。

`Data` ブロックの例を `%text` を使用すると以下のように書くことが出来ます。

```sh
Describe "sort command"
  Data
    #|2
    #|3
    #|1
  End

  expected() {
    %text
    #|1
    #|2
    #|3
  }

  It 'sorts data'
    When call sort
    The output should equal "$(expected)"
  End
End
```

## モック・スタブ

テストではしばしばモックやスタブを使用する必要が出てきます。現在 ShellSpec にはそのための専用の機能はありませんが、関数を一時的に置き換えることで実現できます。

以下は `date` コマンドのスタブをシェル関数で作成する例です。

```sh
Describe 'mock stub sample'
  unixtime() { date +%s; }
  get_next_day() { echo $(($(unixtime) + 86400)); }

  Example 'redefine date command'
    date() { echo 1000000000; }
    When call get_next_day
    The stdout should eq 1000086400
  End

  Example 'use the date command'
    # date is not redefined because this is another subshell
    When call unixtime
    The stdout should not eq 1000000000
  End
End
```

`get_next_day` 関数は内部で `date` コマンドを呼び出しています。しかし直接 `date` コマンドを呼び出してしまうと実行するたびに値が変わるためテストになりません。そのため `date` コマンドを `date` 関数で上書きしています。これはサブシェル内の**一時的な**上書きであることに注意してください。そのためサブシェルを抜けると元の `date` コマンドが呼び出されるようになります。

この例では実際には外部コマンドを関数で上書きしており、関数を関数で上書きしたわけではありませんが、例えば unixtime 関数をブロック内で再定義してもサブシェルを抜けると元に戻ります。

ブロック構造で上書きの範囲が明確になるため直感的に記述することが出来ます。

## パラメーター化テスト (2019/08/23追記)

パラメータを定義することで同様のテストを複数回実行させる機能です。

まず、パラメータ化テストを行わない場合の例です。

```sh
Describe 'example'
  Example "example #1"
    When call echo "$((10 + 20))"
    The output should eq "30"
  End

  Example "example #2"
    When call echo "$((100 + 200))"
    The output should eq "300"
  End
End
```

これをパラメータ化テストに置き換えると以下のようになります。

```sh
Describe 'example'
  Parameters
    "#1" 10 20 30
    "#2" 100 200 300
  End

  Example "example $1"
    When call echo "$(($2 + $3))"
    The output should eq "$4"
  End
End
```

`Parameters`でパラメータを複数定義し、テストはパラメータの部分を `$1`, `$2`, ... で置き換えるだけで、簡単にパラメータ化テストに変更することが出来ます。

パラメータの定義方法は現在以下の4つの方法をサポートしています。

```sh
# block style (default: same as Parameters)
Parameters:block
  "#1" 1 2 3
  "#2" 1 2 3
End

# value style
Parameters:value foo bar baz

# matrix style
# 以下のように解釈されます
#   foo 1
#   foo 2
#   bar 1
#   bar 2
Parameters:matrix
  foo bar
  1 2
End

# dynamic style
# コードで動的にパラメータを生成できます。
Parameters:dynamic
  for i in 1 2 3; do
    %data "#$i" 1 2 3
  done
End
```
## 最後に

こうしてみると色々と機能があるのですが、スペックファイルの文法はシンプルで自然言語に近いため覚えやすく簡単に使えると思います。[その他のサンプル](https://github.com/ko1nksm/shellspec/tree/master/sample/spec) もこちらに用意しています。shUnit2 や Bats は実際に使ってみるとわかると思いますが、テストコードを読むのにシェルスクリプトの知識が必要になります。ShellSpec ではそれが必要最小限に抑えられているためレビューも容易になると思います。

また殆どの処理をシェルの内部コマンドだけで行っており、外部コマンドの呼び出しをほとんど行わないため極めて軽快に動作します。（テスト内容によるためはっきりとは言えませんが、shUnit2 や Bats の2～5倍以上の速度で動作するようです。2019/04/09 追記 0.10.0でサポート予定の並列実行機能でさらに2倍程度高速化します。）高速にテストを実行できることもテストツールの重要な要件の一つです。

シェルスクリプトでテストコードが必要なほど大きなものは作らないという考え方もあるかと思いますがテストは重要です。もし必要になったときは思い出してぜひ使ってみてください。