📑

【RSpec入門】モックって何がすごいの?メソッドの呼び出しを「監視」する仕組みを理解しよう

に公開

スタブとモックの違いが分からない...そんなあなたへ

前回の記事でRSpecのスタブについて学びましたが、今回は「モック」について解説します。

「スタブもモックも同じようなものでしょ?」と思っていませんか?
実は、全然違うんです

簡単に言うと...

  • スタブ = 「こう呼ばれたら、この値を返してね」(偽の返答者
  • モック = 「このメソッドが正しく呼ばれたかチェックして!」(監視役

具体例で理解しよう:メール送信システム

レストランの注文システムで例えてみましょう。

登場人物

  • お客さん(テストケース)
  • ウェイター(UserRegisterクラス):注文を受けて厨房に伝える
  • 厨房(EmailServiceクラス):実際にメール(料理)を作る

実際のコードで見てみよう

1. メール送信サービス(厨房の役割)

# email_service.rb
class EmailService
  def send_welcome_mail(email)
    # 実際のメール送信処理
    puts "Welcome mail sent to #{email}"
    # 本当はSendGridやSESなどを使ってメール送信
  end
end

2. ユーザー登録サービス(ウェイターの役割)

# user_register.rb
require_relative './email_service'

class UserRegister
  def initialize(email_service)
    @email_service = email_service
  end
  
  def register(email)
    puts 'ユーザー登録中...'
    
    # ここが重要!メール送信を依頼
    @email_service.send_welcome_mail(email)
    
    puts 'ユーザー登録完了!'
  end
end

スタブだけだと何が困るの?

まず、スタブだけでテストを書いてみましょう。

# スタブだけのテスト(問題あり)
RSpec.describe UserRegister do
  describe '#register' do
    it 'ユーザー登録ができる' do
      # EmailServiceをスタブ化
      email_service = double(EmailService)
      allow(email_service).to receive(:send_welcome_mail).and_return(true)
      
      user_register = UserRegister.new(email_service)
      user_register.register('test@example.com')
      
      # でも、実際にメール送信メソッドが呼ばれたかは分からない...🤔
    end
  end
end

問題点

このテストでは以下のことが確認できません:

  • ✅ ユーザー登録処理は動く(当たり前)
  • ❌ 実際にメール送信メソッドが呼ばれたか?
  • ❌ 正しいメールアドレスで呼ばれたか?
  • ❌ 何回呼ばれたか?

つまり、「ウェイターが厨房に注文を伝えたか」が分からない!

モックの登場!監視役として活躍

# モックを使ったテスト(解決!)
RSpec.describe UserRegister do
  describe '#register' do
    it 'ウェルカムメールが送信される' do
      # EmailServiceのモックを作成
      email_service = double(EmailService)
      
      # ここがモックの真骨頂!「このメソッドが呼ばれることを期待する」
      expect(email_service).to receive(:send_welcome_mail).with('test@example.com')
      
      user_register = UserRegister.new(email_service)
      user_register.register('test@example.com')
      
      # テストが成功 = メソッドが正しく呼ばれた証拠!
    end
  end
end

モックの書き方を分解して理解

expect(email_service).to receive(:send_welcome_mail).with('test@example.com')

これを日本語にすると:

  • expect(email_service) → "email_serviceに対して"
  • .to receive(:send_welcome_mail) → "send_welcome_mailメソッドが呼ばれることを期待する"
  • .with('test@example.com') → "引数は'test@example.com'で"

重要なポイント

# ❌ 間違った順序
user_register.register('test@example.com')  # 先に実行
expect(email_service).to receive(:send_welcome_mail)  # 後で期待

# ✅ 正しい順序
expect(email_service).to receive(:send_welcome_mail)  # 先に期待
user_register.register('test@example.com')  # 後で実行

「期待してから実行」が鉄則!

モックが検証してくれること

1. メソッドが呼ばれたか?

it 'メール送信メソッドが呼ばれる' do
  email_service = double(EmailService)
  expect(email_service).to receive(:send_welcome_mail)
  
  user_register = UserRegister.new(email_service)
  user_register.register('test@example.com')
  
  # メソッドが呼ばれなければテスト失敗!
end

2. 正しい引数で呼ばれたか?

it '正しいメールアドレスで送信される' do
  email_service = double(EmailService)
  expect(email_service).to receive(:send_welcome_mail).with('correct@example.com')
  
  user_register = UserRegister.new(email_service)
  user_register.register('wrong@example.com')  # わざと違うアドレス
  
  # 引数が違うのでテスト失敗!
end

3. 何回呼ばれたか?

it 'メール送信は1回だけ行われる' do
  email_service = double(EmailService)
  expect(email_service).to receive(:send_welcome_mail).once
  
  user_register = UserRegister.new(email_service)
  user_register.register('test@example.com')
  
  # 1回以外の回数だとテスト失敗!
end

実行回数のいろいろなパターン

# 1回だけ
expect(service).to receive(:method).once

# 2回だけ
expect(service).to receive(:method).twice

# 3回だけ
expect(service).to receive(:method).exactly(3).times

# 最低1回
expect(service).to receive(:method).at_least(1).times

# 最大2回
expect(service).to receive(:method).at_most(2).times

# 呼ばれないことを期待
expect(service).not_to receive(:method)

実際のバグを発見してみよう

バグが含まれたコードを見てみましょう。

# バグありのUserRegisterクラス
class UserRegister
  def initialize(email_service)
    @email_service = email_service
  end
  
  def register(email)
    puts 'ユーザー登録中...'
    
    # バグ:メール送信を忘れている!
    # @email_service.send_welcome_mail(email)  # この行がコメントアウトされている
    
    puts 'ユーザー登録完了!'
  end
end

スタブだけのテスト(バグを発見できない)

it 'ユーザー登録ができる' do
  email_service = double(EmailService)
  allow(email_service).to receive(:send_welcome_mail).and_return(true)
  
  user_register = UserRegister.new(email_service)
  user_register.register('test@example.com')
  
  # テスト成功してしまう...バグなのに!😱
end

モックを使ったテスト(バグを発見!)

it 'ウェルカムメールが送信される' do
  email_service = double(EmailService)
  expect(email_service).to receive(:send_welcome_mail).with('test@example.com')
  
  user_register = UserRegister.new(email_service)
  user_register.register('test@example.com')
  
  # テスト失敗!「send_welcome_mailが呼ばれなかった」エラー
  # バグを発見できた!🎉
end

モックとスタブを組み合わせよう

実際の開発では、モックとスタブを組み合わせることが多いです。

it '登録後にユーザーIDが返される' do
  email_service = double(EmailService)
  
  # モック:メソッドが呼ばれることを検証
  expect(email_service).to receive(:send_welcome_mail).with('test@example.com')
  
  # スタブ:戻り値を設定(必要に応じて)
  allow(email_service).to receive(:send_welcome_mail).and_return(true)
  
  user_register = UserRegister.new(email_service)
  result = user_register.register('test@example.com')
  
  expect(result).to be_truthy
end

モックのメリット・デメリット

👍 メリット

  • バグを発見: メソッドの呼び出し忘れを検出
  • 仕様の保証: 正しい引数でメソッドが呼ばれることを保証
  • 設計の改善: 依存関係を意識するようになる
  • ドキュメント代わり: テストを見れば、どのメソッドが呼ばれるべきかが分かる

👎 デメリット

  • テストが脆い: メソッド名を変更すると多くのテストが壊れる
  • 実装に依存: 内部実装の詳細をテストしてしまう
  • 過度な使用: すべてをモックにすると、実際の動作が分からなくなる

モックを使うべき場面

🟢 モックが適している場面

  • 外部サービス(メール送信、SMS送信、決済処理など)の呼び出し
  • 副作用のある処理(データベース更新、ファイル書き込みなど)
  • 重要な連携処理(他のクラスとの協調動作)

🔴 モックを使わない方が良い場面

  • 単純な計算処理
  • 文字列操作
  • 自分のアプリケーション内の単純なメソッド呼び出し

実践的なコツ

1. 適切な粒度でモックを作る

# ❌ 細かすぎるモック
expect(string).to receive(:upcase).and_return("TEST")
expect(string).to receive(:strip).and_return("TEST")

# ✅ 適切な粒度
expect(email_service).to receive(:send_welcome_mail)

2. have_receivedを使った事後検証

it 'メール送信が行われた' do
  email_service = double(EmailService)
  allow(email_service).to receive(:send_welcome_mail)
  
  user_register = UserRegister.new(email_service)
  user_register.register('test@example.com')
  
  # 事後にメソッドが呼ばれたかチェック
  expect(email_service).to have_received(:send_welcome_mail).with('test@example.com')
end

3. 複数のメソッド呼び出しを検証

it '登録処理で必要な処理がすべて呼ばれる' do
  email_service = double(EmailService)
  logger = double('Logger')
  
  expect(email_service).to receive(:send_welcome_mail).with('test@example.com')
  expect(logger).to receive(:info).with('User registered: test@example.com')
  
  user_register = UserRegister.new(email_service, logger)
  user_register.register('test@example.com')
end

まとめ

モックは「メソッドの呼び出しを監視する」ための仕組みです。

覚えておきたいポイント

  1. スタブ = 値を返す、モック = 呼び出しを検証
  2. expect(...).to receive(...) で呼び出しを期待
  3. 期待を設定してから、テスト対象を実行
  4. バグの早期発見に効果的
  5. 外部サービスとの連携処理で威力を発揮

モックを使うことで、「このメソッドは確実に呼ばれるべき」という仕様をテストで保証できるようになります。

特に、メール送信や決済処理など、「呼び忘れると大問題」になる処理では、モックが強力な味方になってくれます!

最初は慣れないかもしれませんが、バグを未然に防ぐ強力な武器として、ぜひマスターしてください 🚀

おまけ:よくある間違いと対処法

❌ よくある間違い

# 実行してから期待(順序が逆!)
user_register.register('test@example.com')
expect(email_service).to receive(:send_welcome_mail)  # これだと動かない

✅ 正しい書き方

# 期待してから実行
expect(email_service).to receive(:send_welcome_mail)
user_register.register('test@example.com')

Happy Testing! 🎉

Discussion