【RSpec入門】instance_doubleで安全なテストを!doubleとの違いを実例で理解しよう
なぜinstance_doubleが必要なの?
前回、RSpecのdouble
(モック)について学びましたが、実はdouble
には重大な欠陥があります。
「えっ、モック便利じゃん!」と思ったあなた、ちょっと待ってください。
実際の開発現場で起きる、こんなヒヤッとする体験談をお聞きください...
実際にあった怖い話 😱
登場人物(再び)
- EmailService:メール送信を担当
- UserRegister:ユーザー登録とメール送信の連携
Phase 1: 最初の実装
# email_service.rb
class EmailService
def send_welcome_mail(email)
puts "Welcome mail sent to #{email}"
# 実際のメール送信処理
end
end
# user_register.rb
class UserRegister
def initialize(email_service)
@email_service = email_service
end
def register(email)
puts 'ユーザー登録中...'
@email_service.send_welcome_mail(email)
puts 'ユーザー登録完了!'
end
end
Phase 2: テストを書く(doubleを使用)
# spec/user_register_spec.rb
RSpec.describe UserRegister do
describe '#register' do
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')
end
end
end
✅ テスト成功! みんなハッピー!
Phase 3: リファクタリング(ここで事件発生)
ある日、先輩エンジニアがこう言いました:
「send_welcome_mail
よりdeliver_welcome_mail
の方が英語として自然だね」
# email_service.rb(メソッド名を変更)
class EmailService
def deliver_welcome_mail(email) # 名前を変更!
puts "Welcome mail sent to #{email}"
end
end
でも、テストのことを忘れて本体のコードだけ変更...
Phase 4: 悲劇の始まり
$ rspec spec/user_register_spec.rb
✅ テスト成功!
「あれ?テスト通ってるから大丈夫でしょ」
Phase 5: 本番環境でエラー発生! 💥
user_register = UserRegister.new(EmailService.new)
user_register.register('real-user@example.com')
# NoMethodError: undefined method `send_welcome_mail' for #<EmailService>
本番環境で大事故発生! 😱
なぜこんなことが起きたの?
doubleの「嘘つき」な性格
通常のdouble
は、どんなメソッド名でも受け入れてしまいます。
# こんな存在しないメソッドでもOKしちゃう
email_service = double(EmailService)
expect(email_service).to receive(:totally_fake_method) # 存在しないメソッド
expect(email_service).to receive(:another_fake_method) # これも存在しない
expect(email_service).to receive(:completely_made_up) # これも!
# テストは成功してしまう... 😰
つまり、double
は:
- 😈 「実際のクラスにあるメソッドかどうかなんて知らないよ〜」
- 😈 「テストで指定されたメソッドなら何でも受け入れるよ〜」
- 😈 「実装が変わっても気づかないよ〜」
instance_doubleが救世主として登場! 🦸♀️
instance_double
は、実際のクラスの仕様をチェックする優秀な監視役です。
同じ例をinstance_doubleで書き直してみよう
RSpec.describe UserRegister do
describe '#register' do
it 'ウェルカムメールが送信される' do
# doubleの代わりにinstance_doubleを使用
email_service = instance_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
EmailServiceのメソッド名を変更した後...
# email_service.rb
class EmailService
def deliver_welcome_mail(email) # 名前変更
puts "Welcome mail sent to #{email}"
end
end
テスト実行すると...
$ rspec spec/user_register_spec.rb
Failure/Error: expect(email_service).to receive(:send_welcome_mail).with('test@example.com')
the EmailService class does not implement the instance method: send_welcome_mail
Finished in 0.01234 seconds (files took 0.12345 seconds to load)
1 example, 1 failure
🎉 テストが失敗!これで事故を防げた!
doubleとinstance_doubleの違いを表で比較
項目 | double | instance_double |
---|---|---|
メソッドの存在チェック | ❌ しない | ✅ する |
引数の数チェック | ❌ しない | ✅ する |
実装変更の検知 | ❌ できない | ✅ できる |
テストの信頼性 | ⚠️ 低い | ✅ 高い |
書きやすさ | ✅ 簡単 | ✅ 簡単(同じ) |
実行速度 | ⚡ 高速 | ⚡ 高速(ほぼ同じ) |
具体例で違いを見てみよう
1. 存在しないメソッドの場合
# EmailServiceクラス(実際の定義)
class EmailService
def deliver_welcome_mail(email)
puts "Welcome mail sent to #{email}"
end
end
# doubleの場合
email_service = double(EmailService)
expect(email_service).to receive(:non_existent_method) # 存在しないメソッド
# ✅ テスト成功(問題!)
# instance_doubleの場合
email_service = instance_double(EmailService)
expect(email_service).to receive(:non_existent_method) # 存在しないメソッド
# ❌ エラー: the EmailService class does not implement the instance method: non_existent_method
2. 引数の数が違う場合
# EmailServiceクラス(引数は1個)
class EmailService
def deliver_welcome_mail(email) # 引数1個
puts "Welcome mail sent to #{email}"
end
end
# doubleの場合
email_service = double(EmailService)
expect(email_service).to receive(:deliver_welcome_mail).with('email', 'extra_arg') # 引数2個
# ✅ テスト成功(問題!)
# instance_doubleの場合
email_service = instance_double(EmailService)
expect(email_service).to receive(:deliver_welcome_mail).with('email', 'extra_arg') # 引数2個
# ❌ エラー: Wrong number of arguments
より複雑な例:SNS投稿サービス
実際のプロジェクトっぽい例を見てみましょう。
サービスクラス
# social_media_service.rb
class SocialMediaService
def post_tweet(message, hashtags = [])
puts "Posting tweet: #{message} #{hashtags.join(' ')}"
end
def upload_image(image_path, caption)
puts "Uploading image: #{image_path} with caption: #{caption}"
end
end
利用側のクラス
# content_publisher.rb
class ContentPublisher
def initialize(social_media_service)
@social_media_service = social_media_service
end
def publish_blog_post(title, content, image_path)
# ツイート投稿
@social_media_service.post_tweet("新しい記事を投稿しました: #{title}", ['#ブログ', '#新着'])
# 画像アップロード
@social_media_service.upload_image(image_path, title)
end
end
テストの比較
doubleを使った場合(危険)
RSpec.describe ContentPublisher do
describe '#publish_blog_post' do
it 'SNSに投稿される' do
social_service = double(SocialMediaService)
# 間違ったメソッド名でもテスト成功してしまう
expect(social_service).to receive(:tweet_post) # 正しくは post_tweet
.with("新しい記事を投稿しました: テスト記事", ['#ブログ', '#新着'])
expect(social_service).to receive(:image_upload) # 正しくは upload_image
.with('/path/to/image.jpg', 'テスト記事')
publisher = ContentPublisher.new(social_service)
publisher.publish_blog_post('テスト記事', '内容', '/path/to/image.jpg')
# テスト成功(でも実際は動かない!)
end
end
end
instance_doubleを使った場合(安全)
RSpec.describe ContentPublisher do
describe '#publish_blog_post' do
it 'SNSに投稿される' do
social_service = instance_double(SocialMediaService)
# 正しいメソッド名じゃないとエラーになる
expect(social_service).to receive(:post_tweet) # ✅ 正しいメソッド名
.with("新しい記事を投稿しました: テスト記事", ['#ブログ', '#新着'])
expect(social_service).to receive(:upload_image) # ✅ 正しいメソッド名
.with('/path/to/image.jpg', 'テスト記事')
publisher = ContentPublisher.new(social_service)
publisher.publish_blog_post('テスト記事', '内容', '/path/to/image.jpg')
# テスト成功(実際にも動く!)
end
end
end
instance_doubleの実践的な使い方
1. 戻り値も設定したい場合
it 'アップロード成功時のレスポンスを返す' do
social_service = instance_double(SocialMediaService)
# メソッドの呼び出しを検証 + 戻り値も設定
expect(social_service).to receive(:upload_image)
.with('/path/to/image.jpg', 'テスト記事')
.and_return({ success: true, url: 'https://example.com/uploaded.jpg' })
publisher = ContentPublisher.new(social_service)
result = publisher.upload_with_response('/path/to/image.jpg', 'テスト記事')
expect(result[:success]).to be true
end
2. 複数回の呼び出し
it '複数の画像をアップロードする' do
social_service = instance_double(SocialMediaService)
expect(social_service).to receive(:upload_image).twice
publisher = ContentPublisher.new(social_service)
publisher.upload_multiple_images(['/image1.jpg', '/image2.jpg'])
end
3. 引数の柔軟なマッチング
it 'どんな画像パスでもアップロードできる' do
social_service = instance_double(SocialMediaService)
# any_args を使って引数を柔軟にマッチ
expect(social_service).to receive(:upload_image).with(any_args)
publisher = ContentPublisher.new(social_service)
publisher.upload_image('/any/path/image.jpg', 'any caption')
end
よくある質問と回答
Q: instance_doubleって書くのが面倒じゃない?
A: 確かに文字数は多いですが、バグを防ぐ価値は計り知れません。
本番環境でのエラーを考えれば、この程度の手間は安いものです!
Q: 全部instance_doubleにすべき?
A: 外部サービス(API、DB、メールなど)は絶対instance_doubleがおすすめ!
内部の簡単なヘルパーメソッドなどは、doubleでも良い場合があります。
Q: 既存のプロジェクトではどうすればいい?
A: 新しいテストはinstance_double
で書き、既存のものは問題が起きたときにinstance_double
に移行するのが現実的です。
メリット・デメリット
👍 instance_doubleのメリット
- 🛡️ 安全性: 実装とテストの不整合を早期発見
- 🔍 信頼性: テストが通れば実際に動く可能性が高い
- 📚 ドキュメント: テストを見れば正しいメソッド名・引数が分かる
- 🚀 リファクタリング: 安心してメソッド名を変更できる
👎 instance_doubleのデメリット
-
📝 文字数:
double
より長い - ⚠️ 依存: 実際のクラスが定義されている必要がある
- 🧠 学習コスト: 概念を理解する必要がある
まとめ:いつ何を使うべき?
🟢 instance_doubleを使うべき場面
- 外部API連携(重要!)
- メール送信サービス
- 決済処理
- データベース操作
- ファイル操作
- 基本的に副作用のある処理全般
🔵 doubleでも良い場面
- 単純な計算処理のモック
- 文字列操作のヘルパー
- 一時的なプロトタイプ作成
- 内部の単純な処理
📋 実践的な判断基準
「このメソッドが実際に存在しなかったら本番環境でエラーになる?」
→ YES なら instance_double
→ NO なら double
でもOK
最後に:実際のコードレビューでよく見るコメント
# ❌ レビューでNGをもらうコード
email_service = double(EmailService)
expect(email_service).to receive(:send_mail) # メソッド名が曖昧
# ✅ レビューでOKをもらうコード
email_service = instance_double(EmailService)
expect(email_service).to receive(:deliver_welcome_mail).with('user@example.com')
レビュアーの心の声:
「instance_doubleを使ってくれてありがとう!これなら安心してマージできるよ 😊」
テストは「未来の自分」や「チームメンバー」への贈り物です。
instance_double
を使って、より安全で信頼できるテストを書いていきましょう!
Happy Testing! 🎉
次回予告: instance_doubleの応用編として、class_double
やobject_double
についても解説予定です。お楽しみに!
Discussion