RSpecでシェルコマンドの振る舞いを記述できるrspec-shell_commandというGemを作った
github.com だいたいREADMEに全部書いてあるが、雑に使用感とか作った経緯とか書いておく。
使い方
テスト全体はこんな感じで記述できる。
require 'rspec/shell_command' describe '`echo hello`' do # これを include した example group 内でのみライブラリが提供する DSL が使える include RSpec::ShellCommand::DSL it 'successes' do # テストしたいコマンドをバッククォート文字列で指定 # success マッチャで対象のコマンドが成功するかを確認 expect(`echo hello`).to success end it 'exit with status code 0' do # exit_with マッチャでステータスコードを確認 expect(`echo hello`).to exit_with(0) end it 'outputs "hello\n" to stdout only' do # 組込みの output マッチャと同じようにコマンドの出力をテストできる expect(`echo hello`).to output("hello\n").to_stdout expect(`echo hello`).not_to output.to_stderr end end
いわゆるComposing Matchersをサポートしているので、こんな書き方もできる。
# 終了ステータスにマッチャエイリアスを指定 expect(`exit 2`).to exit_with(a_value_between(1, 3)) # 正規表現で指定したり、and/orで連結もできる expect(`echo hello`).to outout(/e.l/).to_stdout & output(a_string_starting_with('he'))
見ての通り、RSpec::ShellCommand::DSL
をinclude
したexample group内ではバッククォート文字列がオーバーライドされる。
バッククォート文字列は以下を保持するラッパーオブジェクトを生成する:
- 与えられたコマンド文字列
- 実行結果(終了ステータス, 標準出力, 標準エラー出力)
与えられたコマンドはその場では実行されず、success
などのマッチャでアサートするときに1度だけ実行される。
実際、上の例の個々のバッククォート文字列で指定したコマンドは、全体で全て1回ずつ実行される。
なので:
it 'output "hello\n" to stdout and exit with status code 2' do command = `echo hello; exit 2` expect(command).to output() expect(command).to exit_with(2) end
と書いても「echo hello; exit 2
」は1回しか実行されない。
バッククォート文字列の結果はオブジェクトなので、当然ワンライナーでも書ける:
describe '`echo hello; exit 2`' do subject { `echo hello; exit 2` } it { is_expected.to output("hello\n").to_stdout } it { is_expected.to exit_with(2) } end
ちなみにoutput
マッチャはブロックを渡せば組込みのoutput
マッチャとして動く。
# どちらもOK it { expect(`echo hello`).to output.to_stdout } it { expect { puts 'hello '}.to output.to_stdout }
そもそもの話
世の中には同じことを考える人は居て、ちょっと調べたら以下が見つかった:
じゃあなんでわざわざ新しい物を作ったかというと「RSpec拡張に興味があった」「CircleCIと連携したGem開発を試してみたかった」というの大きい。
で、最近シェルスクリプトのテストについて考える機会が多かったので「じゃあRSpecで書きやすくしてみるかー」と思い至った次第。
一応、rspec-shell_command
はRSpec標準に近い記法だったり、各種マッチャがcomposableだったりするのが利点……だと思う。
あと逆クォート文字列なら式展開が使えたりヒアドキュメント記法が使えたり(ヒアドキュメントを使うことはまずなさそうだが……)。
ちなみにrspec-command
の存在を知らなかったので、当初はrspec-command
という名前で作っていた。
そしてgem push
する時に被っていることに気づいて、慌ててコード全体を修正するハメになった。
極めて初歩的 & 致命的なミスなので、ちゃんと調べましょうね……
「そもそもシェルスクリプトのテストはシェルスクリプトで書くべきでは?」というツッコミは禁句。
まとめ
RSpec標準に近い記法で外部コマンドがテストできるrspec-shell_command
を作った。
あまり深く考えずに「RSpec拡張作ってみるかー」みたいなノリで始めたが、
- クラスベースなカスタムマッチャの作り方
- RSpec拡張の全体設計
に関してはきちんと調べてきちんと考えないといけなかった。気力と時間があれば記事にしたい。
ついでにGithubとCicleCIを使った自動テスト・自動リリースを試してみたら思ったよりも躓いた。のでこれも気力と時間があれば(ry
ぱっと思いつくTODOは以下の通り: