ShellSpecの新しいQuick実行機能を使った開発ワークフロー(RSpecの--only-failures相当)


はじめに

ShellSpec はシェルスクリプト用のBDDテスティングフレームワークです。RSpec のシェルスクリプト版と言えるものを目指して開発しています。今回 RSpec の --only-failures (--next-failure) 相当の機能である Quick 実行を実装した 0.23.0 をリリースしたのでその解説したいと思います。

RSpec の --only-failures とは?

--only-failures とは前回テスト実行時に失敗したテストだけを再実行するための便利な機能です。開発中にテストが失敗することはよくあることで、失敗したテストだけを再実行できるためテスト実行時間を短縮することが出来ます。(--next-failure--only-failures の機能に加えて最初の失敗で停止させることができます。)

不満点

なぜ RSpec の話から始めたのかと言うと --only-failures は確かに便利な機能なのですが、私にはいくつか不満点があったからです。

spec_helper.rb に設定が必要

--only-failures を使うには前回実行した結果を保存しておく必要があるため spec_helper.rb に以下のような設定が必要です。

spec_helper.rb
RSpec.configure do |config|
  config.example_status_persistence_file_path = "spec/examples.txt"
end

これはプロジェクトで一回だけ行えばいいだけとはいえ手間がかかります。また spec_helper.rb はリポジトリにコミットするものなのでプロジェクト全体で有効になってしまいます。プロジェクト全体でやり方を統一できるというメリットはあるかもしれませんが --only-failures の機能を使うかどうかは各開発者がそれぞれ決めることだと思うので共通のモジュールには設定せずに使えるようにしたいと思っていました。(上記のようなコードを書いた Ruby スクリプトファイルを作って $HOME/.rspec.rspec-local で自分だけ --require すれば自分だけ有効にしたり無効にしたり出来ますがやはり手間です。)

--only-failures オプションを指定するのに判断が必要

--only-failures を指定するかどうかは前回実行してエラーがあったという事実を覚えておいて使うべきかどうかを開発者が判断する必要があります。覚えておけばいいと言われればそれまでなんですが、集中してエラーを修正していて、いざ実行してから「あ、--only-failures つけて実行すればよかった。」と気づくということはよくある話です。

--only-failures という単語が長い

RSpec 3.7.0 で --next-failure-n というエイリアスができたのでマシになりましたが、--only-failures は長いままです。わかりやすいとは思うのですがタイプするのは面倒です。もっとも今の所は他にかぶるオプションがないので --only (--on) と --next (--ne) だけで実行できるんですがw

Quick 実行 (Quick Execution) について

ShellSpec の Quick 実行は RSpec で私が抱いていた不満点を解決できるような形で実装しています。まず根本的な設計の違いとして RSpec の --only-failures は失敗したテストを修正するための機能ですが ShellSpec の Quick 実行は失敗したテストの修正に加え開発全般を効率化する機能にしています。具体的に言うと再実行は「失敗したテスト」だけではなく「一時的なペンディグ」も対象としています。

一時的なペンディング

「一時的なペンディング」とはメッセージなしの Pending のことです。もともと RSpec で Skip の引数のメッセージを省略した場合に temporarily skipped と表示されたり、xittemporarily pending と表現していたりしたので ShellSpec もそれに習ってそのようなメッセージを表示していたりしていたのですが、今回この temporarily pendingtemporarily skip を正式な機能として昇格させました。

「一時的なペンディング」とそうでない普通の「ペンディング」の違いですが、通常の(メッセージありの) Skip(条件付きの Skip if 含む) と Pending は長期的な Skip / Pending で解決までにある程度時間がかかるものとして定義しており、リポジトリにコミットする可能性があるという扱いです。言い換えると何のメッセージも書いてなければあとで見た時にわからないから、リポジトリにコミットするのであれば、きちんとメッセージ(理由)を書きましょうということです。それに対してメッセージなしの SkipPending は現在の作業中に一時的に行うものでリポジトリにコミットしてはいけません。(Todo も内部的にはペンディングとして処理されるのですが、こちらも同様にメッセージがありなしでペンディングの種類が変わります。またブロックの中身がない未実装の Example は通常のペンディング扱いです。)

また追加の機能として --skip-message quiet (以前から実装済み)と --pending-message quiet 、そして両方を合わせてた --quiet というオプションを追加しました。これはスキップとペンディングのメッセージを非表示にするものですが一時的なスキップとペンディングは非表示には出来ません。非表示にしてしまったら気づかずにコミットしてしまうでしょうし、現在やってる作業なので隠す理由がないはずなのであえてそうしています。

もう一つ面白い機能として、一時的なスキップとペンディングの"コメント"をレポートに表示する機能を実装しています。

Pending "外部ライブラリのバグが修正されれば自動的に解決されるはず"
Pending # 来週ここから作業を始める

一時的でないペンディング(上の行)は引数でメッセージ(理由)を渡します。このメッセージは ShellSpec 実行結果のレポートに表示されます。一時的なペンディングは引数のメッセージがありません。しかし一時的なペンディングであっても自分のためにメッセージ(コメント)を残したいと思うことがあるでしょう。その場合に下の行のようにコメントの形で書いていたとしてもレポートに表示されます。

--quick

話がそれてしまいましたが、Quick 実行では「失敗したテスト」だけではなく「一時的なペンディング」も再実行の対象とすることで開発中の機能に関しても、そのテストだけを再実行することが可能になっています。また「失敗したテスト」と「一時的なペンディング」が一つもない場合は全てのテストを実行します。つまりテスト実行対象を自動的に切り替えるので --quick (-q) オプションは常に有効にしたままでよいということです。ただし前回の実行で失敗したテストがある場合に全てのテストを実行したい場合は、--no-quick (+q) オプションを指定する必要があります。ですが --quick$HOME/.shellspec.shellspec-local に書いておいて、普段は何も考えずにただ shellspec と実行すれば大抵の場合は十分なはずです。

--repair, --next

最初は --quick だけを実装しようと考えていたのですが、やはり(一時的なペンディングを除く)失敗したテストだけを実行したいなと思ったため、結局 RSpec の --only-failures--next-failure (-n) に相当する --repair--next (-n) も実装しました。ただし一時的なペンディングが存在しなければ動作は --quick とそう変わらないので --repair はそれほど出番はないと思います。違いとしては前回のテストで失敗したテストがない場合はなにも行わないぐらいです。--next に関しても --fail-fast (と --random none)を指定してるのと変わりません。つまりこの2つはより手軽に失敗したテストだけを修正するための機能ということになります。

余談ですが --repair-r というエイリアスを作りたかったのですが --require に取られているので互換性のために"今回は"見送りました。--require の方の -r を非推奨にしたので次回入れ替える予定です。(--require なんて spec_helper.sh の読み込みにしか使ってないでしょうし、どうせ .shellspec に記述するはずなので -r と短くする必要もないでしょう?)

Quick Mode

説明を飛ばしていましたが、実は --quick を指定して実行すると、自動的に Quick Mode が有効になり、プロジェクトのルートディレクトリに .shellspec-quick.log が生成されます。(正確には .shellspec-quick.log の有無で Quick Mode の有効・無効が決まります。)このファイルが RSpec の config.example_status_persistence_file_path で指定するファイル相当で前回の実行結果が記録されています。RSpec とは違いファイル名は決め打ちです。

注意 前回の実行結果が .shellspec-quick.log に記録(更新)される条件は Quick Mode が有効(つまり.shellspec-quick.log が存在している)場合です。--quick オプションを指定した場合ではありません。そのため一旦 Quick Modeが有効になっているなら --quick オプションを指定しなくても更新されます。--quick オプションはパスしてないテストを再実行するためのオプションです。

Quick Mode を無効にしたい場合は、手動で .shellspec-quick.log を削除して下さい。またこのファイルはリポジトリにコミットしないように、.gitignore などで無視させます。

開発ワークフロー

Quick 実行機能を使った開発ワークフローについてまとめます。

  1. --quick$HOME/.shellspec 等に加えて常に Quick 実行が行われるようにします。(推奨ですが一応任意)
  2. 新しい関数を実装する場合は、そのテストを書き一時的なペンディング(メッセージなしのPending)にします。
  3. --quick ありで)shellspec を実行すると自動的に Quick Mode が有効になりテストが実行されます。
  4. 失敗したテストまたは一時的なペンディングがあれば .shellspec-quick.log に記録されます。
  5. 失敗したテストに関するコードの修正または関数の実装を行います。
  6. 再度(--quick ありで)shellspec を実行すると失敗したテストまたは一時的なペンディングだけが実行されます。
  7. テストに通ったら Pending は削除します。これを繰り返して開発を行います。
  8. 途中、失敗したテストだけを先に修正したい場合は、--repair--next を使用することもできます。

さいごに

この機能は最初の頃から実装したいと思っていて、それに必要な ID 指定によるテスト対象の絞り込みは随分前から実装していたのですが RSpec の実装に満足しておらず良い方法が思いついていなかったのでこれまで手を付けてきませんでした。今回それがようやく実装できたので、実行側(文法ではなくshellspec コマンドの方)は RSpec にだいたい追いつけたんじゃないかと思っています。なにせ --help で表示されるヘルプの行数が Rspec 3.9 は 72 行ですが、ShellSpec は 111 行ですからw。まあ RSpec ではやっていないカバレッジ機能の統合をしてたりするからなのですが、あまりにも酷いので -h でショートヘルプを表示するようにしました。これで 67 行で RSpec を下回りました!(そういう勝負ではない)今回は各フィルタ系オプションも(RSpec にならって)ショートオプションを追加しており、さらに使いやすくなったと思います。説明は長くなりましたが使うのは簡単、--quick オプションを指定するだけです。

補足 RSpec にある機能で実装していないものに --bisect があります。実装が大変そうなのと私が必要性をあまり認識できていないので今の所実装する予定はありません。

おまけ

ここでこっそり --boost オプションという謎の機能について解説します。--boost は CPU の周波数を強制的に引き上げテスト実行速度をアップさせる(ネタ)機能です。周波数を引き上げると言ってもオーバークロックしているのではありません。ただ無限ループするプロセスをバックグラウンドで起動しているだけです。最近の CPU は省電力を持っており周波数が動的に変わります。しかし ShellSpec を単に実行しただけではこの周波数は最高にはなりません。(環境や設定依存だとは思いますが。)--boost オプションは無限ループするプロセスを起動して CPU を使わせることで周波数を強制的に最大に引き上げます。実はもともとプロファイラ機能がそれに近いことをやっており --boost オプションはプロファイラのレポート表示件数を 0 件で実行しているだけです。なぜか仕組み的に CPU を過剰に消費するはずのプロファイラ機能を有効にしたほうがテストが速く終ることに気づいたために生まれた副産物です。