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

## はじめに

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

## RSpec の --only-failures とは？

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

### 不満点

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

#### spec_helper.rb に設定が必要

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

```ruby: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* と表示されたり、`xit` を *temporarily pending* と表現していたりしたので ShellSpec もそれに習ってそのようなメッセージを表示していたりしていたのですが、今回この *temporarily pending* と *temporarily skip* を正式な機能として昇格させました。

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

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

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

```sh
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`）を指定してるのと変わりません。つまりこの２つはより手軽に失敗したテストだけを修正するための機能ということになります。

余談ですが `--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 行ですからｗ。まあ RSpec ではやっていないカバレッジ機能の統合をしてたりするからなのですが、あまりにも酷いので `-h` でショートヘルプを表示するようにしました。これで 67 行で RSpec を下回りました！（そういう勝負ではない）今回は各フィルタ系オプションも（RSpec にならって）ショートオプションを追加しており、さらに使いやすくなったと思います。説明は長くなりましたが使うのは簡単、`--quick` オプションを指定するだけです。

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

### おまけ

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