RailsプロジェクトのRSpec3.0.0へのアップデート

皆さん、こんにちわ。MUGENUPの narikazu です。6月2日にRSpec 3.0.0がリリースされました(Myron Marston » RSpec 2.99.0 and 3.0.0 have been released!)。RSpec 3.0.0での変更点はMyron Marston » Notable Changes in RSpec 3あるいは、その日本語訳のRSpec 3の重要な変更 - 有頂天Rubyをご参照下さい。

ここでは、弊社Railsプロジェクト(Rails 3.2.18)でのRSpec3.0.0へのアップデート手順を記述いたします。この記事が皆さんのアップデート時の参考になれば幸いです。

アップデート手順

アップデート手順は以下の記事を参考に致しました。

正直、ほとんど上記記事と同じ手順を踏んでいます(汗)。しかし、異なる点もあるので、以下手順の大まかな流れをご説明します。

  1. 現在のテストが全てsuccessすることを確認
  2. rspec-railsのバージョンを2.99にアップデート
  3. 2.99の状態でテストを実行
  4. テスト結果のdeprecationを全て修正(transpecと必要なGemのインストールおよび手作業)
  5. rpsec-railsのバージョンを3.0.1にアップデート
  6. 3.0.1の状態でテストを実行
  7. テスト結果のdeprecationを全て修正(transpecと手作業)

以下では、各手順について詳細に記述していきます。

現在のテストが全てsuccessすることを確認

弊社では、CircleCIへブランチをpushすることをトリガーにテストを実行しています。以前はローカル環境にて並列でテストを高速でブン回すparallel_testsを使用しておりました。CircleCIはテスト結果がGithub上で表示されるので、とても便利ですね〜。少し話は逸れましたが、私の場合はRSpecアップデート用のブランチを作成し、pushしている間にrspec-railsのバージョンを上げる作業をしていました。

rspec-railsのバージョンを2.99にアップデート

Gemfileでrspec-railsのバージョンを2.99.0と指定し、bundle update rspecを実行します(bundle update rspec-railsではないので注意)。

# Gemfile
group :test, :development do
  gem 'rspec-rails', '~> 2.99.0'
  # ...
end

2.99の状態でテストを実行

RSpec 2.99でどのようなdeprecation出るか確認するために、全体テストを行います。私の場合はRSpec 2.99アップデートのcommitを積んでGithubにブランチをpushし、CircleCIがテストをブン回してくれました。

テスト結果のdeprecatedを全て修正(transpecと必要なGemのインストールおよび手作業)

2.99で出たdeprecationを放置して、3.0.0に上げるとそのテストは3.0.0に準拠した記述方法ではないのでテストは落ちてしまいます。よって、出てきたdeprecationは全てこのタイミングで修正しましょう。ちなみに、私は258個のdeprecationがありました。

2.99でのdeprecationの修正は以下の流れで行いました。

  1. RSpec3.0で外部化されたGemのインストール
  2. GemのTranspecを使って全自動でRSpec3.0系の記述方法に移行
  3. Transpecで対応できなかった箇所を手作業で修正

以下で各手順について詳細にご説明します。

RSpec3.0で外部化されたGemのインストール

`mock_model` is deprecated. Use the `rspec-activemodel-mocks` gem instead.

上記のdeprecationが出力された方はいらっしゃらないでしょうか?RSpec2.0系で使用できたmock_modelはRSpec3.0では使用できなくなり、rspec-activemodel-mocksとして外部Gemになりました。よって、このgemをGemfileに記述し、bundle installでインストールしましょう。

# Gemfile
group :test do
  gem 'rspec-activemodel-mocks'
  # ...
end

また下記のdeprecationが出力された方もいらっしゃるのではないでしょうか?

`expect(collection).to have(1).attachment` is deprecated. Use the rspec-collection_matchers gem or replace your expectation with something like `expect(collection.size).to eq(1)`

RSpec 2系でcollectionの数を検証するためにexpect(object).to have(2).itemsという構文がありました。RSpec 3ではこの構文はなくなり、expect(object.items.size).to eq 2と書く必要があります。個人的には、RSpec 2系の書き方は自然言語の英語のように書けて気に入っているので、従来の書き方を続けたいと思いました。そのような人のために、rspec-collection_matchersというGemが存在します。これもGemfileに記述し、bundle installを使ってインストールしましょう。

# Gemfile
group :test do
  gem "rspec-collection_matchers"
  # ...
end

弊社のRailsプロジェクトでは使用していませんでしたが、its記法もRSpec 3では使用できなくなり外部Gem化(rspec/rspec-its)しているようです。必要な方はインストールするとよいでしょう!

GemのTranspecを使って全自動でRSpec3.0系の記述方法に移行

RSpec2系を3.0の記述方法にコマンド一発で自動で移行してくれる神Gem transpecがあります。こちらをインストールし、コマンドでサクッと3.0の記述方法に書き変えましょう。インストールはgem i transpecで行います。使い方も簡単で私の場合はtranspec --no-explicit-spec-type --keep have_itemsというコマンドを打って一気に書き換えました。

注意していただきたいのは--no-explicit-spec-type--keep have_itemsオプションをつけていることです。

--no-explicit-spec-typeオプション

2系ではspecファイルが配置されているディレクトリから自動的にspecのタイプ(model, controller, feature等)を判別してくれました。しかし、RSpec 3ではこの機能がオフになっています。よって、Rspec 3ではspecタイプを明示する必要があります。引き続き配置しているディレクトリに応じてspecタイプを自動判別させるためには、spec_helper.rbinfer_spec_type_from_file_location!オプションを付けます。Transpecは自動的にspecタイプのメタデータを追加します。しかし、2系と同じ挙動が気に入っているので、--no-explicit-spec-typeオプションを指定して実行することで、specタイプのメタデータ追加が無効になり、代わりにinfer_spec_type_from_file_location!を自動的に追加します。

--keep have_itemsオプション

expect(object).to have(2).items構文を引き続き使用するべくrspec-collection_matchersのGemを入れた方は、Transpecでhave().item記法を変更させないように--keep have_itemsオプションを付けましょう。

Transpecで対応できなかった箇所を手作業で修正

いくつかTranspecで書換えを行った後もdeprecationが残っていたので、手で書き換えました。ここでは全てのdeprecationを修正しておきましょう。

rpsec-railsのバージョンを3.0.1にアップデート

2.99.0にアップデートした時と同様にGemfileでrspec-railsのバージョンを3.0.1と指定し、bundle update rspecを実行します(bundle update rspec-railsではないので注意)。

# Gemfile
group :test, :development do
  gem 'rspec-rails', '~> 3.0.1'
  # ...
end

3.0.1にアップデート後、再度全テストを通します。

テスト結果のdeprecationを全て修正(transpecと手作業)

3.0.1アップデート後にテストを実行すると以下のエラーで落ちました。

`raise_too_low_error': You are using capybara 2.1.0. RSpec requires version >= 2.2.0. (RSpec::Support::LibraryVersionTooLowError)

Gemのcapybaraのバージョンが低いと言われたので、bundle update capybaraでアップデートしました。

また以下のようなdeprecationが表示されました。

The Fuubar formatter uses the deprecated formatter interface not supported directly by RSpec 3.  To continue to use this formatter you must install the `rspec-legacy_formatters` gem, which provides support for legacy formatters or upgrade the formatter to a compatible version.

これはテストの進捗状況を可視化するgem fuubar(Ruby - Rspecのテスト進捗状況が可視化できるFuubarがいい感じ! - Qiita)のバージョンが低いため表示されます。Rspec 3に対応した2.0.0.rc1を指定しbundle update fuubarでアップデートしました。

# Gemfile
group :test, :development do
  gem 'fuubar', '~> 2.0.0.rc1'
end

そして、再度transpecを用いてdeprecationを消していきます。 なぜかというと2系のstub_chain記法がallow(obj).to receive_message_chain()という記法になるのですがRspec 3.0からでないと使えないため、3.0にアップデート後もtranspecを使用します。前回と同様transpec --no-explicit-spec-type --keep have_itemsコマンドを実行致しました。

これでほとんどのdeprecationが消えました。

また今回のRailsプロジェクトでは本来は通るfeatureテストが時々落ちることがあるので、一度落ちたテストをもう一度実行するモンキーパッチ(sorah / auto_retry.rb)を使わせていただいていました。しかし、Rspec3.0アップデートのapi変更でエラーが出るようになっていたので、少し書き換えました。以下のコードにより、featureスペックかit "hoge", auto_retry: true do ... endとしているテストは落ちるともう一度だけテストを実行してくれます。

# spec/support/auto_retry.rb

module RSpec
  module Core
    class Example
      def run_with_retry(example_group_instance, reporter)
        @retrying_count = 0
        succeeded = run_without_retry(example_group_instance, reporter)

        unless succeeded
          return finish_without_retry(reporter) unless retry_needed?

          $stderr.puts "[RETRY] #{self.location}: #{self.full_description}"
          @retrying_count += 1

          # Initialize itself (originally this has done in ExampleGroup#run_example)
          @exception = nil
          # before_all_ivarsをbefore_context_ivarsに書き換え
          example_group_instance = example_group_instance.class.new.tap do |group|
            group.class.set_ivars(group, group.class.before_context_ivars)
          end

          succeeded = run_without_retry(example_group_instance, reporter)
        end

        succeeded
      end

      alias_method :run_without_retry, :run
      alias_method :run, :run_with_retry

      private

      def finish_with_retry(reporter)
        if (@retrying_count || 0) < 1 && @exception
          false
        else
          finish_without_retry(reporter)
        end

      end

      alias finish_without_retry finish
      alias finish finish_with_retry

      def retry_needed?
        # 元々は@optionsで[:auto_retry]としていたが、metadata[:auto_retry]に書き換え
        metadata[:auto_retry] == true || file_path =~ /spec\/features/
      end
    end
  end
end

その他にはguardのバージョンが低くて動かないようになっていたので、最新のものにアップデートしRspec3でも動くようにしました。

まとめ

以上、弊社のRailsアプリでのRspec3.0へのアップデート手順を記述してみました。作業自体は2〜3時間で終わりました。transpecという神Gemがあるので思ったよりも大変ではなかったですので、皆さんもアップデートしてはいかがでしょうか?

またここで網羅できなかったことおよび、基本的な手順は以下の記事から得ることができます。