この記事はRuby Advent Calendar 2013の6日目の記事です。
昨日はShindo200さんのRuby で paiza.jp のオンラインハッカソン問題に挑戦するときに少し役に立ちそうなことでした。
概要
Rubyのデファクトスタンダードなテストフレームワークと言えるRSpecですが、現在バージョン3.0のリリースへ向けて開発が進められており、先日2013年11月8日には3.0.0.beta1がリリースされました。
この記事ではRSpec 3における変更点と、RSpec 3へのアップグレード手順、また既存のspecを最新の記法に変換するツールを紹介します。
追記
RSpec 3は2014年6月2日に正式リリースされました。この記事は2013年12月6日に書かれたものですが、正式版においても通用する内容になっています。
正式版における主要な変更点は、以下のページが参考になるでしょう。
RSpec 3での変更
RSpec 3は、2010年10月の2.0リリース以来3年ぶりのメジャーバージョンアップとなるため、多くのdeprecatedな機能が削除されます。
The Plan for RSpec 3
RSpec 3がどのようなものになるかは、RSpecプロジェクトのリードメンテナMyron Marston氏が今年2013年7月に記事を書いています。
その後、基本的にはこの計画通りに開発は進んでおり、3.0.0.beta1ではこのうちの一部が実装されています。具体的には、 _What’s Being Removed_で挙げられている項目は全て実装(というか削除)が完了しています。また _What’s New_の項目に関しては以下の3つが実装されています。
- Mocks: Test double interface verification(Mocks: テストダブルのインターフェースの検証)
- Core: DSL methods will yield the example(Core: DSLメソッドがexampleをyieldするようになる)
-
Mocks: any_instance 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記法が追加されます。
# 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記法ですが、この時点ではrspec-expectations
のみへの導入でした。その1年後の2013年7月にRSpec 2.14でrspec-mocks
にもexpect記法が導入され、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
が追加されます。
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記法が無効化されていても利用することができました。
しかしコードの見た目上expect記法と混在させた時に一貫性がなかったり、「ワンライナーshould
のexpect記法は?」といったユーザからの声が多かったため、is_expected.to
が代替記法として追加されます。
ちなみにモンキーパッチshould
はRSpec 3.0からdeprecated扱いになりますが(ただし明示的にshould記法を利用する宣言をすれば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を作りました。Transpecを既存のプロジェクトに対して実行すると、静的解析と動的解析によってspecファイルを最新の記法に書き換えます。現時点で、RSpec 2.0から3.0にかけてdeprecatedになった大半の記法の変換をサポートしています。詳細はREADME - Supported Conversionsを参照して下さい。
プロジェクトによってはTranspecによる変換だけでRSpec 3対応が完了する場合もあるでしょうし、追加で手作業が必要な場合もごくわずかな書き換えで済むかと思います。また、TranspecはRSpecプロジェクトそのものでも利用されています。
手順
RSpec公式のアップグレードガイドがあるので、この手順に従います。
- https://relishapp.com/rspec/docs/upgrade (rspec、rspec-rails共通)
- https://www.relishapp.com/rspec/rspec-rails/docs/upgrade (rspec-rails固有の特記事項)
- 現状のままspecを実行し、all greenなことを確認します。
-
Gemfile
でrspec
のバージョンに~> 2.99
を指定し、bundle update rspec
を実行します。 - 再度specを実行し、all greenなことを確認します。RSpecはSemantic Versioning準拠で開発されているため、RSpec 2.0時代のspecであっても2.99で正常に動作するはずです。動作しないのであれば、それはRSpecのバグです。必要であればこの時点でコミットをします。
- spec実行後、deprecation warningが表示されるので内容を確認します。これらが3.0対応にあたって変更が必要な点になります。
-
gem install transpec
でTranspecをインストールします。これは日常的に使うツールではないので、通常はプロジェクトのGemfile
に追加する必要はありません。 - プロジェクトのルートディレクトリで
transpec
を実行します。Transpecはデフォルトで可能な限り最新の推奨された記法に変換しますが、必要であればTranspecのREADMEを参照し、コマンドラインオプションで変換の挙動をカスタマイズして下さい。例えばhave(n).items
やits
はRSpec 3.0で削除されますが、外部gem化されたrspec-collection_matchersやrspec-itsを利用することで3.0以降も使い続けることができるため、もしその方針であればこれらの変換を--keep have_items,its
オプションで無効化できます。 - Transpecによる変換が完了したら、一旦コミットします(Transpecは自動的にコミットメッセージを生成するのでそれを利用すると良いでしょう)。
- 再度specを実行します。まだdeprecation warningが表示されるようであれば、手作業で対処します。all greenかつdeprecation warningがなくなったらコミットします。
-
Gemfile
で、rspec
のバージョンに~> 3.0
を指定し、bundle update rspec
します。 - RSpec 3.0でspecを実行します。all greenなはずですが、もし失敗するのであればそれはRSpecのバグです。問題なければコミットします。
- RSpec 3.0から新たにdeprecated扱いになる機能もあり、場合によってはwarningが表示されるかもしれません。また、2.99の時点では代替となる記法が存在せず、3.0になってからでないと変換ができないものもあります(前述の
receive_messages
など)。その場合は再度transpec
を実行するとそれらを変換できます。 - 再度specを実行します。問題なければコミットします。
- 完了!
おわりに
本記事で解説したRSpec 3での変更は確定した訳ではなく、今後も新たな変更がある可能性があります。ちなみにGitHubのRSpecのマイルストーンを見る限り、3.0.0.beta2は2013年内のリリースを目指している模様です。