[Rails5] RSpecでモック化の処理を複数テスト間で共有する

## 背景
[Stripe](https://github.com/stripe/stripe-ruby)を利用したアプリケーションのテストを書きたいと思った時に、テストが走る度に毎回 Stripe にリクエストが送信されるのを防ぐため、 Stripe クラスの各メソッドをモック化する必要があった。
example ごとに毎回モック化の処理を書くわけにはいかないので、モック化の処理を複数のファイル間で共有できるように考えてみた。

## 環境
```bash
$ bundle exec rails -v
Rails 5.2.0

$ bundle exec rspec -v
RSpec 3.7
  - rspec-core 3.7.1
  - rspec-expectations 3.7.0
  - rspec-mocks 3.7.0
  - rspec-rails 3.7.2
  - rspec-support 3.7.1
```

## 結論
- `spec/support/` 配下に `disable_stripe_macros.rb` を配置して、共通処理を擁する module を定義する。
- `spec/rails_helper` で上記モジュールを include する
- `spec/requests/xxxx/create_spec.rb` 内で `before { disable_stripe }` のように共通処理を呼び出す

この手順でモック化の処理を共有できた。

## 事前準備
- 必要なgemは下記の通り。

```ruby:Gemfile
gem 'stripe'

group :development, :test do
  gem 'rspec-rails'
end
```

- `rails_helper` の下記の行のコメントアウトを外す。

```ruby:spec/rails_helper
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
```

## 1. 共通処理を定義する
### 目標
- `Stripe::Account`, `Stripe::Plan`, `Stripe::Customer` 等のStripeが提供するクラスについて、 `retrive` や `create` といった頻繁に利用するメソッドをモック化する。

### 方針
- `double` を用いてレスポンスをモック化し、上記のクラスメソッドが呼び出された時にそのモックを返すように設定する。

### ソースコード
```ruby:spec/support/disable_stripe_macros.rb
module DisableStripeMacros
  def disable_stripe
    # 返り値のモックを作る
    response = double("response mock")
    allow(response).to receive(:id).and_return("sample-id")
    allow(response).to receive(:to_json).and_return("{}")

    # 対象のクラスメソッドをまとめてモック化する
    classes = [Stripe::Account, Stripe::Plan, Stripe::Customer]
    method_names = [:create, :retrieve]

    classes.each do |klass|
      method_names.each do |method_name|
        allow(klass).to receive(method_name).and_return(response)
      end
    end
  end
end
```

### 解説
```ruby
# 返り値のモックを作る
response = double("response mock")
allow(response).to receive(:id).and_return("sample-id")
allow(response).to receive(:to_json).and_return("{}")
```

`double` を用いてモックオブジェクトを作成し、アプリケーション内でよく使っていた response.id と response.to_json が返り値を持つように設定する。

```ruby
# 対象のクラスメソッドをまとめてモック化する
classes = [Stripe::Account, Stripe::Plan, Stripe::Customer]
method_names = [:create, :retrieve]

classes.each do |klass|
  method_names.each do |method_name|
    allow(klass).to receive(method_name).and_return(response)
  end
end
```

例えば `allow(Stripe::Account).to receive(:create).and_return(response)` と書けば、 `Stripe::Account.create` がモックオブジェクト `response` を返すようになる。
それを、よく使うメソッドをまとめてモック化できるようにループして回しているだけ。

## 2. 共通処理を include する
`rails_helper` に下記を追加すると、各テスト用のファイルで `disable_stripe` を呼び出せるようになる。

```ruby:spec/rails_helper
RSpec.configure do |config|
  # 下記を追加する
  config.include DisableStripeMacros
end
```

## 3. 共通処理を呼び出す
テスト用のファイルで呼び出してみる。

```ruby:spec/requests/xxxx/create_spec.rb
require "rails_helper"

describe "XxxxController#create" do
  before { disable_stripe } # モック化の処理を呼び出す

  it "returns mock object" do
    it "returns mock object" do
      response = Stripe::Account.retrieve("xxx")
      expect(response.id).to eq("sample-id")
      expect(response.to_json).to eq("{}")
    end
  end
end
```

テストを実行してみる。

```bash
$ bundle exec rspec spec/requests/xxxx/create_spec.rb
XxxxController#create
  returns mock object

Finished in 0.0336 seconds (files took 1.44 seconds to load)
1 example, 0 failures
```

無事にモック化に成功した模様。

## 注意点
せっかくなので、上記の結論に辿り着くまでに試行錯誤する中で分かったことも追記。
最初は `shared_context` を利用すれば実現できるかなと思っていたが、この方法には下記2つの問題があった。

- `double` は example group 内では呼び出せない
- `shared_context` は example group 内でしか呼び出せない

### 詳細
例えば、下記のように `shared_context` を定義する。

```ruby:spec/support/disable_stripe.rb
shared_context "disable stripe" do
  response = double("response mock")
  allow(response).to receive(:id).and_return("sample-id")

  allow(Stripe::Account).to receive(:create).and_return(response)
end
```

これを下記のように呼び出す。

```ruby:spec/requests/xxxx/create_spec.rb
require "rails_helper"

describe "XxxxController#create" do
  include_context "disable stripe" # 共通処理を呼び出そうとする

  it "returns mock object" do
    it "returns mock object" do
      response = Stripe::Account.create("xxx")
      expect(response.id).to eq("sample-id")
    end
  end
end
```

これを実行してみる。

```bash
$ bundle exec rspec spec/requests/xxxx/create_spec.rb

An error occurred while loading ./spec/requests/xxxx/create_spec.rb
Failure/Error: response = double("response mock")
  `double` is not available on an example group (e.g. a `describe` or `context` block). It is only available from within individual examples (e.g. `it` blocks) or from constructs that run in the scope of an example (e.g. `before`, `let`, etc).
```

「`double` は `describe` や `context` ブロック内では呼び出せないから、 `it` や `before` や `let` の中で呼び出しなさいよ。」とのこと。
ならばということで、 `before` ブロックに移動してみる。

```ruby:spec/requests/xxxx/create_spec.rb
require "rails_helper"

describe "XxxxController#create" do
  before do
    include_context "disable stripe"
  end

  it "returns mock object" do
    it "returns mock object" do
      response = Stripe::Account.create("xxx")
      expect(response.id).to eq("sample-id")
    end
  end
end
```

これを実行してみる。

```bash
$ bundle exec rspec spec/requests/xxxx/create_spec.rb

XxxxController#create
  returns mock object (FAILED - 1)

Failures:

  1) XxxxController#create returns mock object
     Failure/Error: include_context "disable stripe"
       `include_context` is not available from within an example (e.g. an `it` block) or from constructs that run in the scope of an example (e.g. `before`, `let`, etc). It is only available on an example group (e.g. a `describe` or `context` block).
     # ./spec/requests/xxxx/create_spec.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.02739 seconds (files took 1.39 seconds to load)
1 example, 1 failure
```

「`include_context` は `it` や `before` や `let` の中では呼び出せないから、  `describe` や `context` ブロック内で呼び出しなさいよ。」とのこと。さっきと真逆じゃないか。
従って、 `double` と `shared_context` は完全に一緒には使えない様子。 module を共有する方針がいいみたい。

## 参考
書いた後に気付いたんだけど、こんなものもあるんだね。

- [stripe-ruby-mock](https://github.com/rebelidealist/stripe-ruby-mock)
- [stripe-mock](https://github.com/stripe/stripe-mock)