ShellSpec - シェルスクリプト用のフル機能のBDDユニットテストフレームワーク

[ShellSpec][shellspec] はシェルスクリプト用に開発した BDD ユニットテストフレームワークです。初期版公開以降、多くの機能を追加しておりフル機能と言えるまでに成長したのですが[公式サイト](https://shellspec.info/)はほとんど更新しておらずその機能を伝えきれなくなっていたので、この度リニューアルしました。ということでその記念として日本語にセルフ翻訳しました。

※ この記事の画像はクリックすると動画で見ることができます。

---

# シェルスクリプトのテストを楽しみましょう！

[ShellSpec][shellspec] は フル機能の BDD ユニットテストフレームワークです。dash, bash, ksh, zsh など 全ての POSIX シェルに対応しており、コードカバレッジ、モック、並列実行、パラメータ化テストなど、高度な機能を提供しています。 クロスプラットフォームで動くシェルスクリプト及びシェルスクリプトライブラリを開発するための、開発・テストツールとして開発しました。多くの実用的な CLI 機能とシンプルでありながら強力な文法によって、楽しいシェルスクリプトテスト環境を提供します。

[shellspec]: https://github.com/shellspec/shellspec

- [なぜ ShellSpec を作ったのか](https://shellspec.info/why.html)
- [ShellSpec と shUnit2 と Bats-core の比較](https://shellspec.info/comparison.html)

## 動作デモ

[![asciicast](https://asciinema.org/a/358055.png)](https://asciinema.org/a/358055?autoplay=1)

ブラウザ上で **[オンラインデモ](https://shellspec.info/demo)** を実行できます。

## 紹介

### ユニットテストフレームワーク

ShellSpec はユニットテストフレームワークであり、特にシェルスクリプト関数を容易にテストできるように設計されています。1 ファイルからなる小さなスクリプトから、複数のファイルで構成される大きなスクリプトまで幅広く対応できます。もちろん外部コマンドの機能テストやシステムテストなど幅広い目的で利用できます。

### 全ての POSIX シェルに対応

ShellSpec は POSIX 準拠の機能を使って実装されており Bash だけでなく全ての POSIX シェルで動作します。例えば POSIX 準拠シェルの dash、古い bash 2.03、最初の POSIX シェルである ksh88、Windows にネイティブで移植されている [busybox-w32](https://frippery.org/busybox/) 等です。複数のシェルと環境に対応するシェルスクリプト開発が簡単にできるようになります。

- <code>bash</code> _>=2.03_, <code>bosh/pbosh</code> _>=2018/10/07_, <code>posh</code> _>=0.3.14_, <code>yash</code> _>=2.29_, <code>zsh</code> _>=3.1.9_
- <code>dash</code> _>=0.5.2_, <code>busybox ash</code> _>=1.10.2_, <code>busybox-w32</code>, <code>GWSH</code> _>=20190627_
- <code>ksh88</code>, <code>ksh93</code> _>=93s_, <code>ksh2020</code>, <code>mksh/lksh</code> _>=R28_, <code>pdksh</code> _>=5.2.14_
- <code>FreeBSD sh</code>, <code>NetBSD sh</code>, <code>OpenBSD ksh</code>, <code>loksh</code>, <code>oksh</code>

### 最小限の必須要件

ほとんどの機能は純粋なシェルスクリプトと少数の基本的な POSIX 準拠のコマンドのみを使って実装されています。そのため小さな Docker イメージや 組込みシステムなど制限がある環境でも動作します。

必須要件: `cat`, `date`, `env`, `ls`, `mkdir`, `od` (or `hexdump`), `rm`, `sleep`, `sort`, `time`

### 多数のシェルでテスト済み

最新のシェルは  CI ([TravisCI](https://travis-ci.com/shellspec/shellspec) / [CirrusCI](https://cirrus-ci.com/github/shellspec/shellspec)) によってテストされています。それに加え過去の Debian で使用されていたシェルでもテストされています。一番古い Debian のバージョンは 2.2 です。

[テスト済みシェルの詳細](https://github.com/shellspec/shellspec/blob/master/docs/shells.md)

### 単一スクリプトファイルのテスト

シェルスクリプトはしばしば 1 つのスクリプトファイルで構成されます。ShellSpec はこのようなスクリプトファイルに含まれるシェル関数のテストやモックもサポートしています。（わずかに書き換えが必要です。）

### DSL 構文

ShellSpec では 独自の DSL でテストコードを書きます。シェルスクリプトと互換性がありシェルスクリプトコードを埋め込むことができます。この自然言語に近い DSL は読みやすさを提供しているだけではありません。目的の１つはシェルスクリプト開発の初心者が陥りやすい[罠][BashPitfalls]を回避することです。またシェルの違いを吸収し、単一のテストコードで複数のシェルに対応した信頼性のあるテストを書くことができます。

[BashPitfalls]: http://mywiki.wooledge.org/BashPitfalls

以下は DSL によって実現されていることの例です。

- スコープとモックに対応したネスト可能なブロック
- Before/After フック、Data ヘルパー、パラメータ化テストなど
- 埋め込みシェルスクリプトを改善するディレクティブ
- `LINENO` 変数に頼らない行番号表示
- シェルオプションの違いの吸収
- シェルに存在するバグの多数のワークアラウンド

```sh
Describe 'sample'
  Describe 'bc command'
    add() { echo "$1 + $2" | bc; }

    It 'performs addition'
      When call add 2 3
      The output should eq 5
    End
  End

  Describe 'implemented by shell function'
    Include ./mylib.sh # add() function defined

    It 'performs addition'
      When call add 2 3
      The output should eq 5
    End
  End
End
```

### モック

二種類のモック機能があります。１つはシェル関数ベースのモックで動作が速くシェルスクリプトライブラリに適しています。もう一つはコマンドベースのモックでより柔軟で外部コマンドをモックするのに適しています。モックはブロックを抜けると自動的に解除されます。そのため解除し忘れるというミスを防ぐことができます。これは他のフレームワークにはない特徴的な機能でモックの利用をよりシンプルにします。

```sh
unixtime() { date +%s; }

Describe 'function-based mock'
  date() {
    echo 1546268400
  }

  It 'is just define a function'
    When call unixtime
    The output should eq 1546268400
  End
End

Describe 'command-based mock'
  Mock date
    echo 1546268400
  End

  It 'creates executable command on the preferred path'
    When call unixtime
    The output should eq 1546268400
  End
End
```

### Data ヘルパー

Data ヘルパー は標準入力データを簡単に提供できる機能です。ヒアドキュメントと違いインデントを壊しません。

```sh
Describe 'Data helper'
  Data # Use Data:expand instead if you want to expand variables.
    #|item1 123
    #|item2 456
    #|item3 789
  End

  It 'provides stdin data'
    When call awk '{total+=$2} END{print total}'
    The output should eq 1368
  End
End
```

### パラメータ化テスト

パラメータ化テスト (またはデータ駆動テスト) を使うと同じテストをパラメータを変えて実行することができます。構文はとてもシンプルで通常のテストを簡単にパラメータ化テストにすることができます。パラメータの定義はマトリクスによる定義やシェルスクリプトコードによる動的な定義にも対応しています。

```sh
Describe 'parameters'
  Parameters
    "#1" 1 2 3
    "#2" 4 5 9
  End

  It "performs a parameterized test ($1)"
    When call echo "$(($2 + $3))"
    The output should eq "$4"
  End
End

Describe 'matrix parameters'
  Parameters:matrix
    foo bar
    1 2
  End

  It "generates matrix parameters ($1 : $2)"
    When call touch "name_$1_$2"
    The file "name_$1_$2" should be exists
  End
End

Describe 'dynamic parameters'
  Parameters:dynamic
    for i in 1 2 3; do
      %data "#$i" "$i" "$(($i*2))" "$(($i + $i*2))"
    done
  End

  It "generates parameter by shell script code ($1)"
    When call echo "$(($2 + $3))"
    The output should eq "$4"
  End
End
```

### ディレクティブ

ディレクティブは埋め込みシェルスクリプトで利用できる便利な命令です。シェルによる動作の違いを吸収したポータブルな `echo` である `%=` (`%putsn`) やインデントを壊すことなく複数行のテキストを出力する `%text` などがあります。これらはテストコードを書くときのシェルスクリプトの問題を解決します。

```sh
Describe 'directives'
  It 'makes embedded shell script easier'
    output() {
      %= "foo"
      %= "bar"
      %= "baz"
    }

    result() {
      %text
      #|foo
      #|bar
      #|baz
    }

    When call output
    The output should eq "$(result)"
  End
End
```

### サンドボックスモード

環境変数 `PATH` を（内部で使用するパスを除いて）空にすることで、外部コマンドが実行されないようにします。これにより開発中に意図せず危険なコマンドを実行してしまうなどというミスを防ぐことができます。このモードではモックの利用が前提となりますが、必要な場合は「サポートコマンド」を利用して外部コマンドを呼び出すこともできます。

```sh
Describe 'sandbox mode'
  sed() { # External command cannot be executed without mock
    @sed "$@" # Run real command
  }

  It 'cannot run external commands without mocking'
    Data "foo"
    When call sed 's/f/F/g'
    The output should eq "Foo"
  End
End
```

*補足: 上記の `@sed` コマンドが「サポートコマンド」で `shellspec --gen-bin @sed` によって生成します。*

### クイックモード

クイックモードを有効にすると、失敗したテストのみを素早く再実行することができます。失敗したテストを再実行すべきかは自動的に判断されるため使用するときの面倒さは一切ありません。

[![asciicast](https://asciinema.org/a/357964.png)](https://asciinema.org/a/357964?autoplay=1)

### 並列実行

テストを繰り返し実行するため実行スピードは重要です。ShellSpec は並列実行を使わずとも快適な速度で動作しますが、並列実行を行うとさらにテストの実行時間を減らすことができます。基本的な POSIX 準拠コマンドのみで実装しているためどの環境でも動作します。

[![asciicast](https://asciinema.org/a/357965.png)](https://asciinema.org/a/357965)

### ランダム実行

テストの実行順をランダムに変更することで、実行順に依存した問題を見つけることができます。またシード値を与えることで以前と同じランダム順で実行することもできます。基本的な POSIX 準拠コマンドのみで実装しているためどの環境でも動作します。

[![asciicast](https://asciinema.org/a/357966.png)](https://asciinema.org/a/357966?autoplay=1)

### 実行トレース

シェルの xtrace 機能を統合し、必要ないトレース出力を抑制することで真に使えるトレース機能を実装しています。デバッグにとても便利です。

[![asciicast](https://asciinema.org/a/357968.png)](https://asciinema.org/a/357968?autoplay=1)

### プロファイラ

プロファイラを使うことで遅いテストを見つけ出し速度を改善することができます。基本的な POSIX 準拠コマンドのみで実装しているためどの環境でも動作します。

[![asciicast](https://asciinema.org/a/357970.png)](https://asciinema.org/a/357970?autoplay=1)

### モダンなレポート出力

テスト結果はモダンでカラフルで読みやすい形で、例えばドットスタイルやドキュメンテーションスタイルでレポートされます。失敗したテストは行番号付きで詳細に出力されるので素早く問題となっているテストを見つけることができます。

#### Progress (dot) フォーマッター

[![asciicast](https://asciinema.org/a/255960.png)](https://asciinema.org/a/255960?autoplay=1)

#### Documentation フォーマッター

[![asciicast](https://asciinema.org/a/255961.png)](https://asciinema.org/a/255961?autoplay=1)

### Generator

テストの結果は画面とは別にファイルに出力することができます。レポーターと同じフォーマッターが利用可能で、TAP ([Test Anything Protocol](https://testanything.org/)) フォーマットや jUnit XML フォーマットを使って CI と簡単に連携できます。一度のテスト実行で複数の形式で出力することができます。

#### TAP フォーマッター

```tap
1..8
ok 1 - calc.sh add() 1 + 1 = 2
ok 2 - calc.sh add() 1 + 10 = 11
ok 3 - calc.sh sub() 1 - 1 = 0
ok 4 - calc.sh sub() 1 - 10 = -9
ok 5 - calc.sh mul() 1 * 1 = 1
ok 6 - calc.sh mul() 1 * 10 = 10
ok 7 - calc.sh div() 1 / 1 = 1
not ok 8 - calc.sh div() 1 / 10 = 0.1 # FAILED
```

#### jUnit XML フォーマッター

```xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="8" failures="1" time="0.31" name="">
  <testsuite id="0" tests="8" failures="1" skipped="0" name="spec/calc_spec.sh" hostname="localhost"
 timestamp="2019-07-06T13:39:59">
    <testcase classname="spec/calc_spec.sh" name="calc.sh add() 1 + 1 = 2"></testcase>
    <testcase classname="spec/calc_spec.sh" name="calc.sh add() 1 + 10 = 11"></testcase>
    <testcase classname="spec/calc_spec.sh" name="calc.sh sub() 1 - 1 = 0"></testcase>
    <testcase classname="spec/calc_spec.sh" name="calc.sh sub() 1 - 10 = -9"></testcase>
    <testcase classname="spec/calc_spec.sh" name="calc.sh mul() 1 * 1 = 1"></testcase>
    <testcase classname="spec/calc_spec.sh" name="calc.sh mul() 1 * 10 = 10"></testcase>
    <testcase classname="spec/calc_spec.sh" name="calc.sh div() 1 / 1 = 1"></testcase>
    <testcase classname="spec/calc_spec.sh" name="calc.sh div() 1 / 10 = 0.1">
      <failure message="The output should equal 0.1">expected: &quot;0.1&quot;
     got: 0

# spec/calc_spec.sh:48</failure>
    </testcase>
  </testsuite>
</testsuites>
```

### コードカバレッジ

[Kcov](https://github.com/SimonKagstrom/kcov)を統合することで簡単にコードカバレッジを計測することができます。コードカバレッジは [HTML](https://circleci.com/api/v1.1/project/github/shellspec/shellspec/latest/artifacts/0/coverage/index.html?branch=master) ファイルや [Coveralls](https://coveralls.io/github/shellspec/shellspec?branch=master)、[Code Climate](https://codeclimate.com/github/shellspec/shellspec)、[Codecov](https://codecov.io/gh/shellspec/shellspec) などのコードカバレッジサービスに対応した形式で出力されます。

*補足: この機能は Bash, Ksh, Zsh のみでサポートしています。また Kcov が必要です。*

[![Coverage report](https://shellspec.info/coverage.png)](https://circleci.com/api/v1.1/project/github/shellspec/shellspec/latest/artifacts/0/coverage/index.html?branch=master)

### Docker コンテナでテストを実行する (実験的機能)

指定した Docker イメージを使ってテストを実行します。同一の環境でテストを実行でき、ホスト環境への影響を心配する必要がありません。

*補足: Docker が必要です。*

[![asciicast](https://asciinema.org/a/357984.png)](https://asciinema.org/a/357984?autoplay=1)

## ShellSpec を使用しているプロジェクト

- [jenkins-x/terraform-google-jx](https://github.com/jenkins-x/terraform-google-jx) - A Terraform module for creating Jenkins X infrastructure on Google Cloud
- [snyk/snyk](https://github.com/snyk/snyk/tree/master/test/smoke) - CLI and build-time tool to find & fix known vulnerabilities in open-source dependencies

## サブプロジェクト

- [ShellMetrics](https://github.com/shellspec/shellmetrics) - シェルスクリプト用の循環的複雑度アナライザー
- [ShellBench](https://github.com/shellspec/shellbench) - POSIX シェル比較用のベンチマークユーティリティー

---

## あとがき

ということで ShellSpec の紹介を兼ねた日本語訳でした。こうしてみると初期に比べて随分と機能が増えました。もちろんこれが全てではなく細かい機能はまだまだあります。デバッガの統合とか Spy 機能の実装とかまだ実装したいものはいくつか残っているのですが主な機能はだいたい実装できたんじゃないかと思っています。

それにともないソースコードの行も（テストコード除いて）8000 行を超えました。最終的には 1 万行超えそうな感じです（笑）。実装している機能の量からするとコンパクトに仕上がってると思ってるんですが、もしかしたらこの規模は POSIX 準拠のシェルスクリプトツールに限定すると最大クラスかもしれません。（bash 専用だと 1 万行超えるツールはいくつかあるようです。）これだけの量を複雑化させずにメンテナンス可能な状態に保つため、シェルスクリプトとしてはかなり独特なコーディングスタイルを使用してます。ちなみに 8000 行というのは空白行なども含めた物理的なソースコードの量で、[ShellMetrics](https://github.com/shellspec/shellmetrics) を使って計測した論理的な行数だと 5000 行程度です。ということでもう一つのツールの宣伝でした。

どれだけの人がこのツールを使っているかはわかりませんが GitHub のスターも増えて Issue もちらほら立てられてるのである程度は使ってる人がいるようです。使っているプロジェクトを GitHub で調べたら Kubernetes に特化した CI/CD ツールの「Jenkins X」と 脆弱性診断ツールの「Snyk」で使われていたので嬉しかったです。これでようやく ShellSpec を使っているプロジェクト名を書くことができるようになりました。

シェルスクリプトでフル機能のテストフレームワークが必要な人は限られてるでしょうしニッチなツールだとは思いますが、シェルスクリプトは開発しづらいとかテストしづらいとか言う人に対しては、ちゃんと設計すればシェルスクリプトでもこれだけのものは作れるしテストも可能であると言えるようになったと思います。