RSpecの最新の動向・RSpec 3へのアップグレードガイド

この記事は[Ruby Advent Calendar 2013](http://qiita.com/advent-calendar/2013/ruby)の6日目の記事です。
昨日はShindo200さんの[Ruby で paiza.jp のオンラインハッカソン問題に挑戦するときに少し役に立ちそうなこと](http://qiita.com/Shindo200/items/a63df49f963f9e3cc441)でした。

## 概要

Rubyのデファクトスタンダードなテストフレームワークと言えるRSpecですが、現在バージョン3.0のリリースへ向けて開発が進められており、先日2013年11月8日には3.0.0.beta1がリリースされました。

この記事ではRSpec 3における変更点と、RSpec 3へのアップグレード手順、また既存のspecを最新の記法に変換するツールを紹介します。

### 追記

RSpec 3は2014年6月2日に正式リリースされました。この記事は2013年12月6日に書かれたものですが、正式版においても通用する内容になっています。

正式版における主要な変更点は、以下のページが参考になるでしょう。

* [Myron Marston » Notable Changes in RSpec 3](http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3)
* [yujinakayama/transpec - Supported Conversions](https://github.com/yujinakayama/transpec#supported-conversions)

## RSpec 3での変更

RSpec 3は、2010年10月の2.0リリース以来3年ぶりのメジャーバージョンアップとなるため、多くのdeprecatedな機能が削除されます。

### The Plan for RSpec 3

RSpec 3がどのようなものになるかは、RSpecプロジェクトのリードメンテナMyron Marston氏が今年2013年7月に記事を書いています。

* [Myron Marston » The Plan for RSpec 3](http://myronmars.to/n/dev-blog/2013/07/the-plan-for-rspec-3)
* [RSpec 3に向けての計画(日本語訳) - 有頂天Ruby](http://nilp.hatenablog.com/entry/2013/07/16/131641)

その後、基本的にはこの計画通りに開発は進んでおり、3.0.0.beta1ではこのうちの一部が実装されています。具体的には、 _What’s Being Removed_で挙げられている項目は全て実装（というか削除）が完了しています。また _What’s New_の項目に関しては以下の3つが実装されています。

* [Mocks: Test double interface verification](http://myronmars.to/n/dev-blog/2013/07/the-plan-for-rspec-3#mocks_test_double_interface_verification)（Mocks: テストダブルのインターフェースの検証）
* [Core: DSL methods will yield the example](http://myronmars.to/n/dev-blog/2013/07/the-plan-for-rspec-3#core_dsl_methods_will_yield_the_example)（Core: DSLメソッドがexampleをyieldするようになる）
* [Mocks: any_instance block implementations will yield the receiver](http://myronmars.to/n/dev-blog/2013/07/the-plan-for-rspec-3#mocks__block_implementations_will_yield_the_receiver)（Mocks: `any_instance`がブロック実装にreceiverをyieldするようになる）

### その後の変更

The Plan for RSpec 3はあくまでも7月時点での計画だったため、その後追加された変更があります。

#### `be_true`/`be_false`マッチャのリネーム

`be_true`/`be_false`マッチャが、`be_truthy`/`be_falsey`（または`be_falsy`）にリネームされ、3.0では`be_true`/`be_false`は利用できなくなります。この変更は既に3.0.0.beta1に取り込まれています。

この理由ですが、

* `be_true`は、その名前にもかかわらず、テスト対象が真（`false`, `nil`以外）であればパスしており、`true`との同一性はテストされていなかった。
* `be_false`も同様に、テスト対象が偽（`false`, `nil`）であればパスしており、`false`との同一性はテストされていなかった。

といったように、名前と挙動が一致していませんでした。

今回の変更は、これまでの挙動に名前を合わせた形になります。実際のところ`be_true`/`be_false`はpredicate method（`?`で終わるメソッド）のテストに使われることが多く、それらは直接`if`文などの条件部に置かれることが多いため、この挙動が問題になるケースはあまりなかったように思います。

既存のspecをどうするべきかですが、この挙動を以前から知っていて敢えて使っていた、もしくは知らなかったけど特にそこまでの厳格さを求めない、ということであれば、`be_truthy`/`be_falsey`に書き換えれば良いでしょう。

一方、そんな挙動だったなんて知らなかった、これを機に厳格にテストしたい、という場合は`be true`/`be false`を利用することが推奨されています。これらは新しい記法ではなく、従来からある`be`マッチャと`true`/`false`リテラルの組み合わせです。`be`は`equal`マッチャのaliasであり、オブジェクトの同一性がテストされます。

#### `rspec-mocks`への一部のexpect記法の追加

should記法の、ハッシュを引数に取った`stub`と、`stub_chain`に対応するexpect記法が追加されます。

```ruby
# should記法
obj.stub(:foo => 1, :bar => 2)
obj.stub_chain(:foo, :bar, :baz)

# expect記法
allow(obj).to receive_messages(:foo => 1, :bar => 2) # 3.0.0.beta1から利用可能
allow(obj).to receive_message_chain(:foo, :bar, :baz) #3.0.0.beta2から利用可能
```

2012年7月に[RSpec 2.11で導入されたexpect記法](http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax)ですが、この時点では`rspec-expectations`のみへの導入でした。その1年後の2013年7月にRSpec 2.14で`rspec-mocks`にも[expect記法が導入](http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/)され、`expect(obj).to receive(:message)`や`allow(obj).to receive(:message)`といった記述が可能になりました。この記法は2.11での導入時ほど話題にならなかったため、知らない方も割といるのではないでしょうか。

しかしRSpec 2.14時点での`rspec-mocks`のexpect記法は、should記法すべてに対して代替手段が用意されている訳ではありませんでした。これは単なる実装漏れではなく、いわゆるcode smellがするため一旦導入を見送っていたなどの理由があったのですが、最終的には前述の2つについては代替記法が導入されることになります。ちなみに`unstub`は未だexpect記法は存在せず、RSpecコアチームの見解を見る限り、おそらく今後も導入されることは無いものと思われます。

#### ワンライナー`should`のexpect記法

ワンライナー`should`のexpect記法として`is_expected.to`が追加されます。

```ruby
it { should be_empty }
it { is_expected.to be_empty } # 2.99.0.beta2から利用可能
```

expect記法が導入された経緯として、既存のshould記法は`should`/`should_receive`/`stub`などのメソッドを`BasicObject`クラスにモンキーパッチで追加するため、delegate/proxyなオブジェクトで正常にテストができないという問題がありました。しかしワンライナー`should`は`BasicObject`クラスにモンキーパッチされたものではなく、その実体は`RSpec::Core::ExampleGroup#should`であり、前述の問題は発生しません。そのためワンライナー`should`はこれまでexpect記法が用意されておらず、`RSpec.configure`で[should記法が無効化](https://www.relishapp.com/rspec/rspec-expectations/v/3-0/docs/syntax-configuration#disable-should-syntax)されていても利用することができました。

しかしコードの見た目上expect記法と混在させた時に一貫性がなかったり、「ワンライナー`should`のexpect記法は？」といったユーザからの声が多かったため、`is_expected.to`が代替記法として追加されます。

ちなみにモンキーパッチ`should`はRSpec 3.0からdeprecated扱いになりますが（ただし[明示的にshould記法を利用する宣言](https://www.relishapp.com/rspec/rspec-expectations/v/3-0/docs/syntax-configuration#disable-expect-syntax)をすればdeprecation warningは表示されない）、ワンライナー`should`は3.0でも現役なため、`is_expected.to`が冗長だと感じる場合はワンライナー`should`を使い続けても問題ありません。

## RSpec 3へのアップグレード

ここでは既存のプロジェクトをRSpec 3にアップグレードする際の手順を解説します。

ベータ版ではありますが、今のところ大きなバグもなく、deprecatedな機能が一掃されたバージョンなので、今のうちにアップグレードしておくと正式リリースの際にスムーズな移行ができます。アグレッシブな方はこの機会にアップグレードしてしまいましょう。

### RSpec 2.99

RSpec 2.99は、バージョン2系との後方互換性を保ちつつ、3.0で非互換になる全ての機能に対してwarningを表示する、アップグレードの通過点となるバージョンです。

既存のspecをまずバージョン2.99で実行することで、そのspecを3.0に対応させるにあたって必要な変更を知ることができます。3.0のchangelog全ての項目に目を通して、どれが自分のプロジェクトに影響を与えるかをいちいち調べる必要はありません。

### Transpec

しかし何を変更すれば良いかはわかっても、その書き換え作業は自分で行う必要があります。正直言ってこれはかなり面倒くさいし、世界中のRSpecユーザがそれぞれ正規表現のワンライナーなどでちまちま置換作業をするのは非合理的です。

という訳で、既存のspecを最新の記法に書き換えるツール、[Transpec](http://yujinakayama.me/transpec/)を作りました。Transpecを既存のプロジェクトに対して実行すると、静的解析と動的解析によってspecファイルを最新の記法に書き換えます。現時点で、RSpec 2.0から3.0にかけてdeprecatedになった大半の記法の変換をサポートしています。詳細は[README - Supported Conversions](https://github.com/yujinakayama/transpec#supported-conversions)を参照して下さい。

プロジェクトによってはTranspecによる変換だけでRSpec 3対応が完了する場合もあるでしょうし、追加で手作業が必要な場合もごくわずかな書き換えで済むかと思います。また、Transpecは[RSpecプロジェクトそのものでも利用されて](https://github.com/rspec/rspec-expectations/commit/f2121650e10464b8cb004e66d03bc77bc7746c6b)います。

### 手順

RSpec公式のアップグレードガイドがあるので、この手順に従います。

* https://relishapp.com/rspec/docs/upgrade （rspec、rspec-rails共通）
* https://www.relishapp.com/rspec/rspec-rails/docs/upgrade （rspec-rails固有の特記事項）

1. 現状のままspecを実行し、all greenなことを確認します。
2. `Gemfile`で`rspec`のバージョンに`~> 2.99`を指定し、`bundle update rspec`を実行します。
3. 再度specを実行し、all greenなことを確認します。RSpecは[Semantic Versioning](http://semver.org/)準拠で開発されているため、RSpec 2.0時代のspecであっても2.99で正常に動作するはずです。動作しないのであれば、それはRSpecのバグです。必要であればこの時点でコミットをします。
4. spec実行後、deprecation warningが表示されるので内容を確認します。これらが3.0対応にあたって変更が必要な点になります。
5. `gem install transpec`でTranspecをインストールします。これは日常的に使うツールではないので、通常はプロジェクトの`Gemfile`に追加する必要はありません。
6. プロジェクトのルートディレクトリで`transpec`を実行します。Transpecはデフォルトで可能な限り最新の推奨された記法に変換しますが、必要であればTranspecの[README](https://github.com/yujinakayama/transpec)を参照し、コマンドラインオプションで変換の挙動をカスタマイズして下さい。例えば`have(n).items`や`its`はRSpec 3.0で削除されますが、外部gem化された[rspec-collection_matchers](https://github.com/rspec/rspec-collection_matchers)や[rspec-its](https://github.com/rspec/rspec-its)を利用することで3.0以降も使い続けることができるため、もしその方針であればこれらの変換を`--keep have_items,its`オプションで無効化できます。
7. Transpecによる変換が完了したら、一旦コミットします（Transpecは[自動的にコミットメッセージを生成する](https://github.com/yujinakayama/transpec#basic-usage)のでそれを利用すると良いでしょう）。
8. 再度specを実行します。まだdeprecation warningが表示されるようであれば、手作業で対処します。all greenかつdeprecation warningがなくなったらコミットします。
9. `Gemfile`で、`rspec`のバージョンに`~> 3.0`を指定し、`bundle update rspec`します。
10. RSpec 3.0でspecを実行します。all greenなはずですが、もし失敗するのであればそれはRSpecのバグです。問題なければコミットします。
11. RSpec 3.0から新たにdeprecated扱いになる機能もあり、場合によってはwarningが表示されるかもしれません。また、2.99の時点では代替となる記法が存在せず、3.0になってからでないと変換ができないものもあります（前述の`receive_messages`など）。その場合は再度`transpec`を実行するとそれらを変換できます。
12. 再度specを実行します。問題なければコミットします。
13. 完了！

## おわりに

本記事で解説したRSpec 3での変更は確定した訳ではなく、今後も新たな変更がある可能性があります。ちなみにGitHubのRSpecのマイルストーンを見る限り、3.0.0.beta2は2013年内のリリースを目指している模様です。