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
上記のメール送信をモックに差し替える場合、下記のようにallow
と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
# 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
上記の外部システムへのアクセスをモックに差し替える場合、下記のようにwith
とand_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