RSpecモックのユースケース

RSpecを使っているとメールや外部システムへのアクセスなどをモックに差し替えることがあると思いますが、毎回書き方を忘れてしまうのでよく使うユースケースをまとめてみました。
今後もよく使うユースケースがあれば追記していきたいと思います。

Rspecのモックについては公式ドキュメントが大変参考になりました。
https://relishapp.com/rspec/rspec-mocks/docs

この記事を書いているときのバージョン

* Ruby: 2.7.2
* Rails: 6.0.3.3
* rspec-rails: 4.0.1

# 特定のクラスメソッドをモックに差し替える

下記のようなユーザー登録処理があるとします。
Userモデルを作成した後に`UserMailer.post`というクラスメソッドを使ってメールを送信しています。

```ruby
class User < ApplicationRecord
  class << self
    def register(name, email)
      user = create!(name: name, email: email)
      # メールを送信
      UserMailer.post(user)
      user
    end
  end
end
```

上記のメール送信をモックに差し替える場合、下記のように`allow`と`receive`を使って書きます。
モックに差し替える処理は、実際の処理`subject`を実行する前に定義しておく必要があるのでご注意ください。

```ruby
RSpec.describe User, type: :model do
  describe '.register' do
    let(:name) { 'hoge' }
    let(:email) { 'hoge@example.com' }

    subject { described_class.register(name, email) }

    it do
      # UserMailer.postをモックにしている
      allow(UserMailer).to receive(:post)

      # subjectのregisterが実行されるが、メール送信はモックに差し替わっているのでメール送信は行われない
      is_expected.to be_truthy
    end
  end
end
```

## sleepをモックに差し替える
sleepは処理と処理の間隔をあけたいときなど明示的に処理を止めるときに使いますが、テスト実行時は不要なことが多いです。
不要な場合にはsleepをモックに差し替えることをオススメします。モックにすることでsleepの時間を短縮することができます。

先程の例で使ったUser.registerにsleepが入っている場合を考えます。

```ruby
class User < ApplicationRecord
  class << self
    def register(name, email)
      user = create!(name: name, email: email)
      # メール送信前に１０秒待つ
      sleep 10
      UserMailer.post(user)
      user
    end
  end
end
```
この場合、subject実行前に`allow(User).to receive(:sleep)`を追加しておくとsleepがモックになり、処理が止まらなくなります。

# インスタンスメソッドをモックに差し替える
今度はインスタンスメソッドをモックに差し替えるユースケースを考えます。

先程とほぼ同じですが、メール送信がインスタンスメソッドになっています。

```ruby
class User < ApplicationRecord
  class << self
    def register(name, email)
      user = create!(name: name, email: email)
      # メールを送信
      UserMailer.new(user).post
      user
    end
  end
end
```

上記のメール送信をモックに差し替える場合、下記のように書きます。
postメソッドだけをモックにするのではなくインスタンス自体をモックにしてレシーバーを定義しています。

```ruby
RSpec.describe User, type: :model do
  describe '.register' do
    let(:name) { 'hoge' }
    let(:email) { 'hoge@example.com' }
    let(:mailer_mock) { double('mailer') }

    subject { described_class.register(name, email) }

    it do
      # インスタンス化したときにmockを返却している
      allow(UserMailer).to receive(:new).and_return(mailer_mock)
      # mailer_mockにpostというレシーバーを定義
      allow(mailer_mock).to receive(:post)

      # subjectのregisterが実行されるが、メール送信はモックに差し替わっているのでメール送信は行われない
      is_expected.to be_truthy
    end
  end
end
```

## インスタンスをモックに差し替えるのが困難な場合
先程は単純な例だったため、`UserMailer.new`で返却されるインスタンスをモックオブジェクトに差し替えて、モックオブジェクトにレシーバーを定義することでテストすることができました。
ただし、次の例ように`UserMailer.new`で作ったインスタンスのメソッドをたくさん呼び出しており、`post`だけモックにしたいときに先程の方法では実装が困難です。

```ruby
mailer = UserMailer.new(user)
mailer.set_title
mailer.set_to
mailer.set_from
...
# ここまでのsetメソッドは実行してpostだけモックに差し替えたい
mailer.post
```

このような場合は`allow_any_instance_of`を使います。
下記のように書くことで、UserMailerのインスタンスのpostだけをモックにすることができます。

```ruby
allow_any_instance_of(UserMailer).to receive(:post)
```

# 戻り値を設定する
「[インスタンスメソッドをモックに差し替える](#インスタンスメソッドをモックに差し替える)」で使っていた`and_return`を使うことでモックに戻り値が設定できます。
一律同じ戻り値を返す場合は先程の例のように`and_return`を使うだけでよいのですが、モックに渡すパラメーターによって戻り値を変えることができるのでそのやり方を紹介します。

下記のようなユーザー登録処理があるとします。
Userモデルの作成に必要なnameを`External.get`を使って外部システムから取得しています。

```ruby
class User < ApplicationRecord
  class << self
    def register(email)
      # emailに対応するnameを外部システムから取得する
      name = External.get(email)
      create!(name: name, email: email)
    end
  end
end
```

上記の外部システムへのアクセスをモックに差し替える場合、下記のように`with`と`and_return`を組み合わせて書きます。

```ruby
RSpec.describe User, type: :model do
  describe '.register' do
    subject { described_class.register(email) }

    before do
      # withでパラメーターを指定し、and_returnでパラメーターに対応する戻り値を設定
      # 'hoge@example.com'の場合は'hoge'を返却
      allow(External).to receive(:get).with('hoge@example.com').and_return('hoge')
      # 'fuga@example.com'の場合は'fuga'を返却
      allow(External).to receive(:get).with('fuga@example.com').and_return('fuga')
    end

    context 'hoge' do
      let(:email) { 'hoge@example.com' }
      it { expect(subject.name).to eq 'hoge' }
    end

    context 'fuga' do
      let(:email) { 'fuga@example.com' }
      it { expect(subject.name).to eq 'fuga' }
    end
  end
end
```

# ブロックのパラメーターを設定する
ブロックをモックに差し替えることができます。

下記のようなユーザー登録処理があるとします。
Userモデルの作成に必要なnameとemailを`External.get`を使って外部システムから取得しています。

```ruby
class User < ApplicationRecord
  class << self
    def register(uri)
      user = nil
      External.get(uri) do |name, email|
        user = create!(name: name, email: email)
      end
      user
    end
  end
end
```

上記の`External.get`のブロックをモックに差し替える場合、下記のように`and_yield`を使って書きます。

```ruby
RSpec.describe User, type: :model do
  describe '.register' do
    let(:uri) { `https://example.com` }
    subject { described_class.register(uri) }

    it do
      # and_yieldでブロックのパラメーターを指定
      allow(External).to receive(:get).and_yield('hoge', 'hoge@example.com')

      user = subject
      # and_yieldで指定したパラメーターが設定されている
      expect(user.name).to eq 'hoge'
      expect(user.email).to eq 'hoge@example.com'
    end
  end
end
```

# 呼び出し回数を検証する
モックが呼び出された回数を検証することができます。

「[特定のクラスメソッドをモックに差し替える](#特定のクラスメソッドをモックに差し替える)」と同じ`User.register`を使います。
`User.register`で呼ばれるメール送信`UserMailer.post`が一度だけ呼ばれていること検証します。
検証では`expect` + `have_received`を使います。

```ruby
RSpec.describe User, type: :model do
  describe '.register' do
    let(:name) { 'hoge' }
    let(:email) { 'hoge@example.com' }

    subject { described_class.register(name, email) }

    it do
      allow(UserMailer).to receive(:post)

      subject
      # UserMailer.postが1回呼び出されていることを検証する
      expect(UserMailer).to have_received(:post).once
    end
  end
end
```

今回は1回だけ呼び出されることの検証するので`once`を使いましたが、それ以外にも様々なカウント方法があります。
詳しくは公式ドキュメントを参照してください。
https://relishapp.com/rspec/rspec-mocks/v/3-10/docs/setting-constraints/receive-counts

## receiveとhave_receivedの違い
検証をするときに`have_received`を使いましたが、上記の例は`receive`でも書くことができます。
この書き方だとモック作成と検証を同時に書くことができて行数は削減できるのですが、subjectの前に検証コードを書かなければいけないため、直感的に理解しづらく個人的にはオススメしません。

```ruby
RSpec.describe User, type: :model do
  describe '.register' do
    let(:name) { 'hoge' }
    let(:email) { 'hoge@example.com' }

    subject { described_class.register(name, email) }

    it do
      expect(UserMailer).to receive(:post).once
      subject
    end
  end
end
```

# パラメーターを検証する
モックを呼び出す際に指定されたパラメーターを検証することができます。

「[特定のクラスメソッドをモックに差し替える](#特定のクラスメソッドをモックに差し替える)」と同じ`User.register`を使います。
`User.register`で呼ばれるメール送信`UserMailer.post`に生成したuserが渡されていることを検証します。
パラメーターの検証は`with`を使います。

```ruby
RSpec.describe User, type: :model do
  describe '.register' do
    let(:name) { 'hoge' }
    let(:email) { 'hoge@example.com' }

    subject { described_class.register(name, email) }

    it do
      allow(UserMailer).to receive(:post)

      user = subject
      # with(user)を使って渡されていることを検証する
      expect(UserMailer).to have_received(:post).with(user)
    end
  end
end
```