Quantcast
Channel: Bashタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 2810

シェルスクリプト用 BDD テスティングフレームワーク shellspec の強力なモック機能について

$
0
0

はじめに

shellspecは私が開発しているシェルスクリプト用 BDD テスティングフレームワークです。今回は shellspec のモック機能がどれだけシンプルで強力であるかを紹介したいと思います。

注意この記事の内容を手元で試す場合は、最新の master か 0.23.0 以降を使用して下さい。printコマンドの再定義が想定されておらず shellspec が正しく動作しない問題がありました。

テスト対象の関数について

まず前提知識として。シェルスクリプトには文字列を(エスケープ文字を解釈せずに)そのまま出力するコマンドとして printfコマンドがあることはご存知だと思います。printfはほとんどのシェルでビルトインコマンドなのですが、mksh ではビルトインコマンドではありません。外部コマンドの printfが使われるので問題なく動くのですが遅くなってしまいます。(Linux 上ではさほど遅くないのですが WSL 上だと出力する量によっては致命的なレベルで遅くなります。)

mksh には代わりに printコマンドがあり、こちらを使っても文字をそのまま出力することができます。そこで bash では printf、mksh では printを使用する putsn関数を実装します。(この関数は、./lib.shに定義されているものとします。)

./lib.sh
putsn(){if["${KSH_VERSION:-}"];then
    putsn(){
      print -r--"$1"}else
    putsn(){printf'%s\n'"$1"}fi
  putsn "$@"}

なんの変哲もない・・・と言いたいところですが、人によってはこれが動くのか?と思うかもしれないですね。シェルスクリプトでは関数の中に関数を書くこともできますし、関数を再定義することもできます。それを利用して最初に putsnが呼ばれたときにシェルを判定し適切な関数を定義して、二度目以降はチェックを行わないようにしています。

テストコード

さて、shellspec を使って、この関数のテストコードを書きましょう。一番シンプルなテストコードです。「putsn関数は文字列を出力する。」という文章のテストで、テスト内容は関数実行の出力を調べているだけの明快なテストコードですね。(疑問が浮かぶ人がいるかも知れないので説明しておくと、このテストコードはシェルスクリプトと互換性がある文法でシェルスクリプトコードを埋め込むこともできますが、そのままシェルで動かしているのではなく shellspec で変換してから実行しています。文法チェックをしてもエラーにはなりませんが bash でそのまま実行することはできません。あと大文字で始まる関数名が気になるかもしれませんが、他のシェル関数と名前がかぶらないようにするためと英文は大文字で始めるものです。と言っておきます。)

spec/putsn_spec.sh
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変数の方から。これは BeforeKSH_VERSIONに値を設定するだけです。(正確にはコードを実行しています。mksh では KSH_VERSIONは読み取り専用なので値が入ってないときだけ代入しています。)変数はすぐに設定されるのではなく(Describeに複数のItがある場合に)It実行の直後で設定されます。また Itのブロックを抜けると変数は元に戻ります。It等のブロックは内部的にサブシェル内で実行されるのでこのような動きになります。

spec/putsn_spec.sh
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の後のテスト内容の文章も適切なものに変更しましょう。

spec/putsn_spec.sh
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

さて最初のテストと合わせて完全なテストコードはこのようになります。

spec/putsn_spec.sh
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 をご存知な方は想像つくと思いますが ContextDescribeの別名です。Describeはテストの対象、Contextは特定の状況、を記述しテストコードを文章として読みやすくするためだけに使い分けます。mksh 版のテストは「putsn() はシェルが mksh の時、print コマンドを使用して文字列を出力する。」という文章のテストになります。

さて話をさかのぼります。冒頭で「putsnは最初に呼ばれたときに適切な関数を定義する」と書きました。mksh 版のテストは「最初にputsnを使う時ではない」と思いませんか?

上の方で「Itのブロックを抜けると KSH_VERSION変数は元に戻る」と書きましたが元に戻るのは変数だけではありません。サブシェル(≒別プロセス)で実行されているので定義した関数なども元に戻ります。(正確にはサブシェルで行った処理は呼び出し元には影響を与えません。)そのため最初の putsnのテストで定義された関数は存在せず、きれいな環境でテストが実行されます。

このようにブロック構造から容易に推測可能な「スコープ」があるかのように動作します。モック関数として定義した print関数も Contextを抜けると消えます。つまりモックを解除するための専用のコマンドもいらないということです。ブロック構造に従うだけで他の部分のテストでの副作用に影響されずに、クリーンな状態でテストを実行することができます。(補足ですが DescriptContextブロックはいくつでもネスト可能ですのでテスト内容を構造化するにに役立ちます。)

さいごに

この記事は shellspec のモック機能の紹介なのですが、実のところモックを実現するための"機能"はありません。モック関数は普通にシェル関数を定義するだけです。あるのはブロック構造とそれをサブシェル実行に変換する仕組みです。これだけでシンプルで強力なモック機能を実現しています。


Viewing all articles
Browse latest Browse all 2810

Trending Articles