読者です 読者をやめる 読者になる 読者になる

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::DSLincludeした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_commandRSpec標準に近い記法だったり、各種マッチャがcomposableだったりするのが利点……だと思う。 あと逆クォート文字列なら式展開が使えたりヒアドキュメント記法が使えたり(ヒアドキュメントを使うことはまずなさそうだが……)。

ちなみにrspec-commandの存在を知らなかったので、当初はrspec-commandという名前で作っていた。 そしてgem pushする時に被っていることに気づいて、慌ててコード全体を修正するハメになった。
極めて初歩的 & 致命的なミスなので、ちゃんと調べましょうね……

「そもそもシェルスクリプトのテストはシェルスクリプトで書くべきでは?」というツッコミは禁句。

まとめ

RSpec標準に近い記法で外部コマンドがテストできるrspec-shell_commandを作った。

あまり深く考えずに「RSpec拡張作ってみるかー」みたいなノリで始めたが、

  • クラスベースなカスタムマッチャの作り方
  • RSpec拡張の全体設計

に関してはきちんと調べてきちんと考えないといけなかった。気力と時間があれば記事にしたい。
ついでにGithubとCicleCIを使った自動テスト・自動リリースを試してみたら思ったよりも躓いた。のでこれも気力と時間があれば(ry

ぱっと思いつくTODOは以下の通り:

  • コマンド実行後の指定したファイル/ディレクトリの有無のテスト
  • 環境変数を明示した対象コマンド実行
  • ファイル, ディレクトリ, 外部コマンドのモック化