Ruby
Rails
RSpec
activesupport
RubyOnRails

(rspec-railsを使わず)RSpecでRailsをテストするTips

何らかの理由でrspec-railsが使えない場合に、純粋なRSpecのみ(rspec-core、rspec-expectations、rspec-mocks)でRuby on Railsのテストコードを記述する方法を調べました。

環境

  • Ruby 2.5.1
  • Ruby on Rails 5.2.0
  • RSpec 3.7.0

準備

適当なRailsプロジェクトを用意して、RSpecをインストールします。

$ echo "gem 'rspec', group: :test" >> Gemfile
$ bundle
$ bundle exec rspec --init
$ mkdir -p spec/models
$ code spec/models/user.rb

テスト対象のコードを記述

scaffoldで適当なモデルを生成します。

$ bundle exec rails generate scaffold User name:string email:string

モデルクラス内に、テスト対象のメソッドを記述します。

app/models/user.rb
class User < ApplicationRecord
  def self.awesome_method
    my_name = find_or_initialize_by(id: 9999).name = Time.zone.now.to_s
    return my_name if my_name.present?
  end
end

このUser::awesome_method()の中身に意味はありませんが、ActiveRecordやActiveSupportの機能を使っています。今回は、このメソッドが文字列を返すことをテストします。

テストコード

先に完成したコードを掲載します。

spec/models/user.rb
require "spec_helper"

# ActiveSupportの読み込み
require 'active_support'
require 'active_support/core_ext'
Time.zone = 'Tokyo'

# ActiveRecordのダミークラス定義
class ApplicationRecord
  self.define_singleton_method(:find_or_initialize_by, ->(v){})
end
require_relative "../../app/models/user"
User.class_eval{ attr_accessor :name }

# RSpec
RSpec.describe User do
  let(:dummy_user) { User.new }
  before do
    allow(User).to receive(:find_or_initialize_by) {|arg| dummy_user }
    allow(dummy_user).to receive(:name= ) {}
  end

  # テストコード
  describe "::awesome_method" do
    subject { User.awesome_method }

    it "文字列を返す" do
      is_expected.to be_kind_of(String)
    end
  end

end

実行した結果です。

$ bundle exec rspec spec/models/user.rb
.

Finished in 0.23986 seconds (files took 19.24 seconds to load)
1 example, 0 failures

以下、簡単な解説です。

ActiveSupportを読み込む

Time.zoneObject.present?はRailsに組み込まれているActiveSupportの機能であり、そのままだとundefined methodエラーになってしまいます。これはActiveSupportをrequireして有効にすればOKです。

require 'active_support'
require 'active_support/core_ext'
Time.zone = 'Tokyo'

参考: Active Support コア拡張機能 | Rails ガイド

ActiveRecordのダミークラス定義

User.find_or_initialize_by(id: 9999).nameの部分は、ActiveRecordが持っているfind_or_initialize_by()name()をそれぞれスタブする必要があります。空のメソッドをダミークラスとして定義しておいて、rspecのallow等でスタブとして挙動を定義します。

実際にはダミークラスはファイルを分けたほうが良いと思います。

class ApplicationRecord
  self.define_singleton_method(:find_or_initialize_by, ->(v){})
end

require_relative "../../app/models/user"
User.class_eval{ attr_accessor :name }
let(:dummy_user) { User.new }
before do
  allow(User).to receive(:find_or_initialize_by) {|arg| dummy_user }
  allow(dummy_user).to receive(:name= ) {}
end

こうすることでrspec-mocksのTest Spies機能なども利用できます。

逆にRSpecでallowする必要がないならば、ダミークラスにメソッドの挙動を定義する方法でもテストコードを実行できます。

class ApplicationRecord
  def self.find_or_initialize_by(arg)
    dummy_user = nil
    def dummy_user.name=(v)
    end
    dummy_user
  end
end

まとめ

結論として、たった少しのテストケースでも手間が必要になりました。できる限りrspec-railsを使ったほうが良いと思いました。

一方で、この方法だとRailsを起動しないので、test environmentの構築が必要無いというメリットはありました。

参考