Ruby
RSpec
RubyDay 6

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

More than 3 years have passed since last update.

この記事は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つが実装されています。

その後の変更

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リテラルの組み合わせです。beequalマッチャの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なオブジェクトで正常にテストができないという問題がありました。しかしワンライナーshouldBasicObjectクラスにモンキーパッチされたものではなく、その実体はRSpec::Core::ExampleGroup#shouldであり、前述の問題は発生しません。そのためワンライナーshouldはこれまでexpect記法が用意されておらず、RSpec.configureshould記法が無効化されていても利用することができました。

しかしコードの見た目上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公式のアップグレードガイドがあるので、この手順に従います。

  1. 現状のままspecを実行し、all greenなことを確認します。
  2. Gemfilerspecのバージョンに~> 2.99を指定し、bundle update rspecを実行します。
  3. 再度specを実行し、all greenなことを確認します。RSpecはSemantic Versioning準拠で開発されているため、RSpec 2.0時代のspecであっても2.99で正常に動作するはずです。動作しないのであれば、それはRSpecのバグです。必要であればこの時点でコミットをします。
  4. spec実行後、deprecation warningが表示されるので内容を確認します。これらが3.0対応にあたって変更が必要な点になります。
  5. gem install transpecでTranspecをインストールします。これは日常的に使うツールではないので、通常はプロジェクトのGemfileに追加する必要はありません。
  6. プロジェクトのルートディレクトリでtranspecを実行します。Transpecはデフォルトで可能な限り最新の推奨された記法に変換しますが、必要であればTranspecのREADMEを参照し、コマンドラインオプションで変換の挙動をカスタマイズして下さい。例えばhave(n).itemsitsはRSpec 3.0で削除されますが、外部gem化されたrspec-collection_matchersrspec-itsを利用することで3.0以降も使い続けることができるため、もしその方針であればこれらの変換を--keep have_items,itsオプションで無効化できます。
  7. Transpecによる変換が完了したら、一旦コミットします(Transpecは自動的にコミットメッセージを生成するのでそれを利用すると良いでしょう)。
  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年内のリリースを目指している模様です。