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

何らかの理由で[rspec\-rails](https://github.com/rspec/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
```

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

```ruby: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の機能を使っています。今回は、このメソッドが文字列を返すことをテストします。

## テストコード

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

```ruby: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.zone`や`Object.present?`はRailsに組み込まれている**ActiveSupport**の機能であり、そのままだとundefined methodエラーになってしまいます。これはActiveSupportをrequireして有効にすればOKです。

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

参考: [Active Support コア拡張機能 \| Rails ガイド](https://railsguides.jp/active_support_core_extensions.html#%E3%82%B3%E3%82%A2%E6%8B%A1%E5%BC%B5%E6%A9%9F%E8%83%BD%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80%E6%96%B9%E6%B3%95)

### ActiveRecordのダミークラス定義

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

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

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

require_relative "../../app/models/user"
User.class_eval{ attr_accessor :name }
```

```ruby
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機能](https://github.com/rspec/rspec-mocks#test-spies)なども利用できます。

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

```ruby
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の構築が必要無いというメリットはありました。

### 参考

- [rspec/rspec: RSpec meta\-gem that depends on the other components](https://github.com/rspec/rspec)
- [最新のrspec\-railsでコントローラのヘルパーメソッドをスタブする \- tohashi's blog](http://64.hateblo.jp/entry/2014/11/29/235823)