【備忘録】doubleとinstance_doubleの違いを「ブログ通知機能」で理解する
はじめに
この記事は、Railsでブログアプリを作っている私が、メール送信ジョブのテストでつまずいた経験をもとにまとめた備忘録です。
RSpecでモック化を初めて行い、 double と instance_doubleについて学びました。
結論:doubleよりinstance_doubleを使ったほうがいいらしい
| 項目 | double | instance_double |
|---|---|---|
| ベースクラス | なし(何でもOK) | 実在するクラスを元に作る |
| 型チェック | なし | メソッド名や引数をチェックしてくれる |
| テストの信頼性 | やや低い(何でも通る) | 高い(本物に近い) |
instance_double を使うと、「実際のクラスに存在しないメソッドを呼んでしまった」などのミスを防げるようです。
実例:ブログ投稿の通知メールを送るジョブ
例えば
ブログ記事が投稿されたら、購読者に「新しい記事が公開されました!」というメールを送るジョブ PostNotificationJob を作成したとする
# app/jobs/post_notification_job.rb
class PostNotificationJob < ApplicationJob
queue_as :default
def perform(post_id)
post = Post.find(post_id)
NotificationMailer.new_post(post).deliver_now!
post.update!(notified: true)
end
end
最初に書いたテスト(doubleを使用)
# spec/jobs/post_notification_job_spec.rb
require "rails_helper"
RSpec.describe PostNotificationJob, type: :job do
describe "#perform" do
it "投稿通知メールを送信する" do
post = create(:post)
mailer_mock = double("NotificationMailer")
allow(NotificationMailer).to receive(:new_post).with(post).and_return(mailer_mock)
allow(mailer_mock).to receive(:deliver_now!)
described_class.new.perform(post.id)
expect(NotificationMailer).to have_received(:new_post).with(post)
expect(mailer_mock).to have_received(:deliver_now!)
end
end
end
上記のコードでも動きはしましたが、落とし穴があるようでした。
問題点:doubleは「何でもできる」オブジェクト
doubleは仮のオブジェクトを作るだけで、実際のActionMailer::MessageDeliveryとは関係がないようです。
テストとしての信頼性が少し落ちてしまいまうために、極力使用しないほうがいいようです。
修正版:instance_doubleを使う
# 修正版(推奨)
require "rails_helper"
RSpec.describe PostNotificationJob, type: :job do
describe "#perform" do
it "投稿通知メールを送信する" do
post = create(:post)
# 実在クラス ActionMailer::MessageDelivery に基づくモック
mailer_mock = instance_double(ActionMailer::MessageDelivery)
allow(NotificationMailer).to receive(:new_post).with(post).and_return(mailer_mock)
allow(mailer_mock).to receive(:deliver_now!)
described_class.new.perform(post.id)
expect(NotificationMailer).to have_received(:new_post).with(post)
expect(mailer_mock).to have_received(:deliver_now!)
end
end
end
メリット
-
ActionMailer::MessageDeliveryに実際に存在するメソッドしかモックできない - 呼び出し忘れやタイプミスを検知できる
- テストの信頼性が上がる
次に送信後の状態更新もテストしてみる
it "送信後に通知済みに更新される" do
post = create(:post, notified: false)
allow(NotificationMailer).to receive_message_chain(:new_post, :deliver_now!)
described_class.new.perform(post.id)
expect(post.reload.notified).to eq(true)
end
ここではメール送信部分をまるごとモックし、「ジョブがDBを正しく更新しているか」だけを確認しているところ
| ポイント | 内容 |
|---|---|
double は自由すぎて危険 |
存在しないメソッドでも通ってしまう |
instance_double は型安全 |
実在クラスに基づき、メソッドの存在も検証 |
| モックは「信頼できる偽物」を作るために使う | 本物の構造に近づけるほどテストが強くなる |
まとめ
RSpecでモックを実装する際、ただ double を使用すれば十分だと考えていました。
しかしながら、学習を進める中で instance_double の重要性を理解するに至りました。
RSpecにおけるモックの実装は、想像以上に奥深く、適切な使い分けには一定の習熟が必要だと感じています。
引き続き、実践を通じて理解を深めていきたいと思います。
※初学者のため、間違えていたらすいません。