0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails初学者】初学者がつまづいたRSpecの落とし穴:doubleは何でもできるけど扱いに注意が必要

Posted at

【備忘録】doubleとinstance_doubleの違いを「ブログ通知機能」で理解する

はじめに

この記事は、Railsでブログアプリを作っている私が、メール送信ジョブのテストでつまずいた経験をもとにまとめた備忘録です。
RSpecでモック化を初めて行い、 doubleinstance_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におけるモックの実装は、想像以上に奥深く、適切な使い分けには一定の習熟が必要だと感じています。
引き続き、実践を通じて理解を深めていきたいと思います。

※初学者のため、間違えていたらすいません。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?