📑
【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)
have_received
を使った事後検証
2. 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
まとめ
モックは「メソッドの呼び出しを監視する」ための仕組みです。
覚えておきたいポイント:
- スタブ = 値を返す、モック = 呼び出しを検証
-
expect(...).to receive(...)
で呼び出しを期待 - 期待を設定してから、テスト対象を実行
- バグの早期発見に効果的
- 外部サービスとの連携処理で威力を発揮
モックを使うことで、「このメソッドは確実に呼ばれるべき」という仕様をテストで保証できるようになります。
特に、メール送信や決済処理など、「呼び忘れると大問題」になる処理では、モックが強力な味方になってくれます!
最初は慣れないかもしれませんが、バグを未然に防ぐ強力な武器として、ぜひマスターしてください 🚀
おまけ:よくある間違いと対処法
❌ よくある間違い
# 実行してから期待(順序が逆!)
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