18
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RailsAdvent Calendar 2020

Day 7

RSpecモックのユースケース

Last updated at Posted at 2020-12-06

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というクラスメソッドを使ってメールを送信しています。

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

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

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が入っている場合を考えます。

class User < ApplicationRecord
  class << self
    def register(name, email)
      user = create!(name: name, email: email)
      # メール送信前に10秒待つ
      sleep 10
      UserMailer.post(user)
      user
    end
  end
end

この場合、subject実行前にallow(User).to receive(:sleep)を追加しておくとsleepがモックになり、処理が止まらなくなります。

インスタンスメソッドをモックに差し替える

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

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

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

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

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だけモックにしたいときに先程の方法では実装が困難です。

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

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

allow_any_instance_of(UserMailer).to receive(:post)

戻り値を設定する

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

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

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

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

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を使って外部システムから取得しています。

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を使って書きます。

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を使います。

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の前に検証コードを書かなければいけないため、直感的に理解しづらく個人的にはオススメしません。

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を使います。

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
18
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?