シェルスクリプト用 BDD テスティングフレームワーク ShellSpec の強力なモック機能について
はじめに
ShellSpec は私が開発しているシェルスクリプト用 BDD テスティングフレームワークです。今回は ShellSpec のモック機能がどれだけシンプルで強力であるかを紹介したいと思います。
注意 この記事の内容を手元で試す場合は、最新の master か 0.23.0 以降を使用して下さい。print
コマンドの再定義が想定されておらず ShellSpec が正しく動作しない問題がありました。
テスト対象の関数について
まず前提知識として。シェルスクリプトには文字列を(エスケープ文字を解釈せずに)そのまま出力するコマンドとして printf
コマンドがあることはご存知だと思います。printf
はほとんどのシェルでビルトインコマンドなのですが、mksh ではビルトインコマンドではありません。外部コマンドの printf
が使われるので問題なく動くのですが遅くなってしまいます。(Linux 上ではさほど遅くないのですが WSL 上だと出力する量によっては致命的なレベルで遅くなります。)
mksh には代わりに print
コマンドがあり、こちらを使っても文字をそのまま出力することができます。そこで bash では printf
、mksh では print
を使用する putsn
関数を実装します。(この関数は、./lib.sh
に定義されているものとします。)
putsn() {
if [ "${KSH_VERSION:-}" ]; then
putsn() {
print -r -- "$1"
}
else
putsn() {
printf '%s\n' "$1"
}
fi
putsn "$@"
}
なんの変哲もない・・・と言いたいところですが、人によってはこれが動くのか?と思うかもしれないですね。シェルスクリプトでは関数の中に関数を書くこともできますし、関数を再定義することもできます。それを利用して最初に putsn
が呼ばれたときにシェルを判定し適切な関数を定義して、二度目以降はチェックを行わないようにしています。
テストコード
さて、ShellSpec を使って、この関数のテストコードを書きましょう。一番シンプルなテストコードです。「putsn
関数は文字列を出力する。」という文章のテストで、テスト内容は関数実行の出力を調べているだけの明快なテストコードですね。(疑問が浮かぶ人がいるかも知れないので説明しておくと、このテストコードはシェルスクリプトと互換性がある文法でシェルスクリプトコードを埋め込むこともできますが、そのままシェルで動かしているのではなく ShellSpec で変換してから実行しています。文法チェックをしてもエラーにはなりませんが bash でそのまま実行することはできません。あと大文字で始まる関数名が気になるかもしれませんが、他のシェル関数と名前がかぶらないようにするためと英文は大文字で始めるものです。と言っておきます。)
Include ./lib.sh
Describe "putsn()"
It "puts string"
When call putsn "test"
The output should eq "test"
End
End
このテストコードは特に問題なくこのままでも十分なのですが ShellSpec はカバレッジを計測することができます。しかし残念ながらカバレッジは bash でしか計測できません。mksh 版の putsn
関数は bash では実行されないので、カバレッジレポートには記録されません。これをなんとかしたいと思います。(もちろん テストとしては mksh を使ってテストコードを実行すれば十分です。ここではカバレッジレポートが気になるというだけの問題です。)
さて bash で putsn() { print -r -- "$1"; }
のコードを実行させるにはどうすればよいでしょうか? 課題は二つあります。一つは bash では KSH_VERSION
変数が定義されていないこと、もう一つは print
コマンドが存在しないことです。
まずは簡単な KSH_VERSION
変数の方から。これは Before
で KSH_VERSION
に値を設定するだけです。(正確にはコードを実行しています。mksh では KSH_VERSION
は読み取り専用なので値が入ってないときだけ代入しています。)変数はすぐに設定されるのではなく(Describe
に複数のIt
がある場合に)It
実行の直後で設定されます。また It
のブロックを抜けると変数は元に戻ります。It
等のブロックは内部的にサブシェル内で実行されるのでこのような動きになります。
Describe "putsn()"
Before ': "${KSH_VERSION:=MIRBSD}"'
It "puts string"
When call putsn "test"
The output should eq "test"
End
End
これで bash で print
コマンドを使用する mksh 版が呼ばれることになりますが、そのままでは print
コマンドはないのでエラーになります。そのためモック関数として print
シェル関数を定義します。モック関数を定義する専用のコマンドがあるわけではなく単純にシェル関数を定義するだけです。ついでにIt
の後のテスト内容の文章も適切なものに変更しましょう。
Describe "putsn()"
Before ': "${KSH_VERSION:=MIRBSD}"'
print() { echo "print command:" "$@"; }
It "puts string using print command"
When call putsn "test"
The output should eq "print command: -r -- test"
End
End
さて最初のテストと合わせて完全なテストコードはこのようになります。
Include ./lib.sh
Describe "putsn()"
It "puts string"
When call putsn "test"
The output should eq "test"
End
Context "when shell is mksh"
Before ': "${KSH_VERSION:=MIRBSD}"'
print() { echo "print command:" "$@"; }
It "puts string using print command"
When call putsn "test"
The output should eq "print command: -r -- test"
End
End
End
ここまでで説明してないものとして Context
が登場しました。Rspec をご存知な方は想像つくと思いますが Context
は Describe
の別名です。Describe
はテストの対象、Context
は特定の状況、を記述しテストコードを文章として読みやすくするためだけに使い分けます。mksh 版のテストは「putsn() はシェルが mksh の時、print コマンドを使用して文字列を出力する。」という文章のテストになります。
さて話をさかのぼります。冒頭で「putsn
は最初に呼ばれたときに適切な関数を定義する」と書きました。mksh 版のテストは「最初にputsn
を使う時ではない」と思いませんか?
上の方で「It
のブロックを抜けると KSH_VERSION
変数は元に戻る」と書きましたが元に戻るのは変数だけではありません。サブシェル(≒別プロセス)で実行されているので定義した関数なども元に戻ります。(正確にはサブシェルで行った処理は呼び出し元には影響を与えません。)そのため最初の putsn
のテストで定義された関数は存在せず、きれいな環境でテストが実行されます。
このようにブロック構造から容易に推測可能な「スコープ」があるかのように動作します。モック関数として定義した print
関数も Context
を抜けると消えます。つまりモックを解除するための専用のコマンドもいらないということです。ブロック構造に従うだけで他の部分のテストでの副作用に影響されずに、クリーンな状態でテストを実行することができます。(補足ですが Descript
、Context
ブロックはいくつでもネスト可能ですのでテスト内容を構造化するにに役立ちます。)
さいごに
この記事は ShellSpec のモック機能の紹介なのですが、実のところモックを実現するための"機能"はありません。モック関数は普通にシェル関数を定義するだけです。あるのはブロック構造とそれをサブシェル実行に変換する仕組みです。これだけでシンプルで強力なモック機能を実現しています。
コマンドベースモック機能 (2020-08-21追記)
「さいごに」でモックを実現するための機能はありませんと書きましたが、バージョン 0.26.0 でモックを実現するための機能が追加されました。上記の内容はシェル関数を定義することでモックを実現するのですが、実はこれでは対応できない場合がいくつか存在します。
- シェル関数では使えない文字が使われているコマンドのモック
- テスト対象が外部コマンド(別プロセス)の場合
- モックから元のコマンドを呼び出したい場合
こういった場合に使えるのがコマンドベースモック機能です。その名の通りシェル関数の代わりに(一時的な)外部コマンドを定義します。使い方は簡単で関数定義の代わりに Mock
ヘルパーを使用して定義するだけです。
Describe 'mock example'
get_next_day() { echo $(($(date +%s) + 86400)); }
Mock date
echo 1546268400
End
It 'runs the mocked date command'
When call get_next_day
The stdout should eq 1546354800
The variable called should eq 1
End
End
実装的には ShellSpec 起動時に PATH にモックコマンド用のパスを優先されるように登録し適宜モックコマンドを作成して優先的に呼び出されるようにしています。シェル関数と同様にブロックスコープと連動しているので内部のブロックで一時的に上書きすることが可能でブロックを抜けたら元に戻るようになっています。
また正確にはモック機能とは独立した機能なのですが、モック関数から元のコマンドを呼び出したい場合、頭に @
をつけて呼び出すことができます。例えば date
コマンドの場合 @date
です。この @
付きのコマンドをサポートコマンドと呼んでおり使用する場合は shellspec --gen-bin
でサポートコマンドを生成する必要があります。サポートコマンドはただの外部コマンドなのでシェル関数ベースのモックでも使うことができます。
Author And Source
この問題について(シェルスクリプト用 BDD テスティングフレームワーク ShellSpec の強力なモック機能について), 我々は、より多くの情報をここで見つけました https://qiita.com/ko1nksm/items/9053e9c1e42a2ae9033e著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .