ShellSpec はシェルスクリプト用に開発した BDD ユニットテストフレームワークです。初期版公開以降、多くの機能を追加しておりフル機能と言えるまでに成長したのですが公式サイトはほとんど更新しておらずその機能を伝えきれなくなっていたので、この度リニューアルしました。ということでその記念として日本語にセルフ翻訳しました。
※ この記事の画像はクリックすると動画で見ることができます。
シェルスクリプトのテストを楽しみましょう!
ShellSpec は フル機能の BDD ユニットテストフレームワークです。dash, bash, ksh, zsh など 全ての POSIX シェルに対応しており、コードカバレッジ、モック、並列実行、パラメータ化テストなど、高度な機能を提供しています。 クロスプラットフォームで動くシェルスクリプト及びシェルスクリプトライブラリを開発するための、開発・テストツールとして開発しました。多くの実用的な CLI 機能とシンプルでありながら強力な文法によって、楽しいシェルスクリプトテスト環境を提供します。
動作デモ
ブラウザ上で オンラインデモ を実行できます。
紹介
ユニットテストフレームワーク
ShellSpec はユニットテストフレームワークであり、特にシェルスクリプト関数を容易にテストできるように設計されています。1 ファイルからなる小さなスクリプトから、複数のファイルで構成される大きなスクリプトまで幅広く対応できます。もちろん外部コマンドの機能テストやシステムテストなど幅広い目的で利用できます。
全ての POSIX シェルに対応
ShellSpec は POSIX 準拠の機能を使って実装されており Bash だけでなく全ての POSIX シェルで動作します。例えば POSIX 準拠シェルの dash、古い bash 2.03、最初の POSIX シェルである ksh88、Windows にネイティブで移植されている busybox-w32 等です。複数のシェルと環境に対応するシェルスクリプト開発が簡単にできるようになります。
-
bash
>=2.03,bosh/pbosh
>=2018/10/07,posh
>=0.3.14,yash
>=2.29,zsh
>=3.1.9 -
dash
>=0.5.2,busybox ash
>=1.10.2,busybox-w32
,GWSH
>=20190627 -
ksh88
,ksh93
>=93s,ksh2020
,mksh/lksh
>=R28,pdksh
>=5.2.14 -
FreeBSD sh
,NetBSD sh
,OpenBSD ksh
,loksh
,oksh
最小限の必須要件
ほとんどの機能は純粋なシェルスクリプトと少数の基本的な POSIX 準拠のコマンドのみを使って実装されています。そのため小さな Docker イメージや 組込みシステムなど制限がある環境でも動作します。
必須要件: cat
, date
, env
, ls
, mkdir
, od
(or hexdump
), rm
, sleep
, sort
, time
多数のシェルでテスト済み
最新のシェルは CI (TravisCI / CirrusCI) によってテストされています。それに加え過去の Debian で使用されていたシェルでもテストされています。一番古い Debian のバージョンは 2.2 です。
単一スクリプトファイルのテスト
シェルスクリプトはしばしば 1 つのスクリプトファイルで構成されます。ShellSpec はこのようなスクリプトファイルに含まれるシェル関数のテストやモックもサポートしています。(わずかに書き換えが必要です。)
DSL 構文
ShellSpec では 独自の DSL でテストコードを書きます。シェルスクリプトと互換性がありシェルスクリプトコードを埋め込むことができます。この自然言語に近い DSL は読みやすさを提供しているだけではありません。目的の1つはシェルスクリプト開発の初心者が陥りやすい罠を回避することです。またシェルの違いを吸収し、単一のテストコードで複数のシェルに対応した信頼性のあるテストを書くことができます。
以下は DSL によって実現されていることの例です。
- スコープとモックに対応したネスト可能なブロック
- Before/After フック、Data ヘルパー、パラメータ化テストなど
- 埋め込みシェルスクリプトを改善するディレクティブ
-
LINENO
変数に頼らない行番号表示 - シェルオプションの違いの吸収
- シェルに存在するバグの多数のワークアラウンド
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
モック
二種類のモック機能があります。1つはシェル関数ベースのモックで動作が速くシェルスクリプトライブラリに適しています。もう一つはコマンドベースのモックでより柔軟で外部コマンドをモックするのに適しています。モックはブロックを抜けると自動的に解除されます。そのため解除し忘れるというミスを防ぐことができます。これは他のフレームワークにはない特徴的な機能でモックの利用をよりシンプルにします。
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 ヘルパー は標準入力データを簡単に提供できる機能です。ヒアドキュメントと違いインデントを壊しません。
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
パラメータ化テスト
パラメータ化テスト (またはデータ駆動テスト) を使うと同じテストをパラメータを変えて実行することができます。構文はとてもシンプルで通常のテストを簡単にパラメータ化テストにすることができます。パラメータの定義はマトリクスによる定義やシェルスクリプトコードによる動的な定義にも対応しています。
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
などがあります。これらはテストコードを書くときのシェルスクリプトの問題を解決します。
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
を(内部で使用するパスを除いて)空にすることで、外部コマンドが実行されないようにします。これにより開発中に意図せず危険なコマンドを実行してしまうなどというミスを防ぐことができます。このモードではモックの利用が前提となりますが、必要な場合は「サポートコマンド」を利用して外部コマンドを呼び出すこともできます。
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
によって生成します。
クイックモード
クイックモードを有効にすると、失敗したテストのみを素早く再実行することができます。失敗したテストを再実行すべきかは自動的に判断されるため使用するときの面倒さは一切ありません。
並列実行
テストを繰り返し実行するため実行スピードは重要です。ShellSpec は並列実行を使わずとも快適な速度で動作しますが、並列実行を行うとさらにテストの実行時間を減らすことができます。基本的な POSIX 準拠コマンドのみで実装しているためどの環境でも動作します。
ランダム実行
テストの実行順をランダムに変更することで、実行順に依存した問題を見つけることができます。またシード値を与えることで以前と同じランダム順で実行することもできます。基本的な POSIX 準拠コマンドのみで実装しているためどの環境でも動作します。
実行トレース
シェルの xtrace 機能を統合し、必要ないトレース出力を抑制することで真に使えるトレース機能を実装しています。デバッグにとても便利です。
プロファイラ
プロファイラを使うことで遅いテストを見つけ出し速度を改善することができます。基本的な POSIX 準拠コマンドのみで実装しているためどの環境でも動作します。
モダンなレポート出力
テスト結果はモダンでカラフルで読みやすい形で、例えばドットスタイルやドキュメンテーションスタイルでレポートされます。失敗したテストは行番号付きで詳細に出力されるので素早く問題となっているテストを見つけることができます。
Progress (dot) フォーマッター
Documentation フォーマッター
Generator
テストの結果は画面とは別にファイルに出力することができます。レポーターと同じフォーマッターが利用可能で、TAP (Test Anything Protocol) フォーマットや jUnit XML フォーマットを使って CI と簡単に連携できます。一度のテスト実行で複数の形式で出力することができます。
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 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: "0.1"
got: 0
# spec/calc_spec.sh:48</failure>
</testcase>
</testsuite>
</testsuites>
コードカバレッジ
Kcovを統合することで簡単にコードカバレッジを計測することができます。コードカバレッジは HTML ファイルや Coveralls、Code Climate、Codecov などのコードカバレッジサービスに対応した形式で出力されます。
補足: この機能は Bash, Ksh, Zsh のみでサポートしています。また Kcov が必要です。
Docker コンテナでテストを実行する (実験的機能)
指定した Docker イメージを使ってテストを実行します。同一の環境でテストを実行でき、ホスト環境への影響を心配する必要がありません。
補足: Docker が必要です。
ShellSpec を使用しているプロジェクト
- jenkins-x/terraform-google-jx - A Terraform module for creating Jenkins X infrastructure on Google Cloud
- snyk/snyk - CLI and build-time tool to find & fix known vulnerabilities in open-source dependencies
サブプロジェクト
- ShellMetrics - シェルスクリプト用の循環的複雑度アナライザー
- ShellBench - POSIX シェル比較用のベンチマークユーティリティー
あとがき
ということで ShellSpec の紹介を兼ねた日本語訳でした。こうしてみると初期に比べて随分と機能が増えました。もちろんこれが全てではなく細かい機能はまだまだあります。デバッガの統合とか Spy 機能の実装とかまだ実装したいものはいくつか残っているのですが主な機能はだいたい実装できたんじゃないかと思っています。
それにともないソースコードの行も(テストコード除いて)8000 行を超えました。最終的には 1 万行超えそうな感じです(笑)。実装している機能の量からするとコンパクトに仕上がってると思ってるんですが、もしかしたらこの規模は POSIX 準拠のシェルスクリプトツールに限定すると最大クラスかもしれません。(bash 専用だと 1 万行超えるツールはいくつかあるようです。)これだけの量を複雑化させずにメンテナンス可能な状態に保つため、シェルスクリプトとしてはかなり独特なコーディングスタイルを使用してます。ちなみに 8000 行というのは空白行なども含めた物理的なソースコードの量で、ShellMetrics を使って計測した論理的な行数だと 5000 行程度です。ということでもう一つのツールの宣伝でした。
どれだけの人がこのツールを使っているかはわかりませんが GitHub のスターも増えて Issue もちらほら立てられてるのである程度は使ってる人がいるようです。使っているプロジェクトを GitHub で調べたら Kubernetes に特化した CI/CD ツールの「Jenkins X」と 脆弱性診断ツールの「Snyk」で使われていたので嬉しかったです。これでようやく ShellSpec を使っているプロジェクト名を書くことができるようになりました。
シェルスクリプトでフル機能のテストフレームワークが必要な人は限られてるでしょうしニッチなツールだとは思いますが、シェルスクリプトは開発しづらいとかテストしづらいとか言う人に対しては、ちゃんと設計すればシェルスクリプトでもこれだけのものは作れるしテストも可能であると言えるようになったと思います。