instance_doubleで安全なモックを使ったテストを書こう
学習の目標
本章では、以下の内容を学習します。
- instance_doubleの基本概念と重要性を理解する
- 通常のdoubleと比較した際のメリットを把握する
- クラスの仕様変更を早期に検知する方法を習得する
- より信頼性の高いテストコードの書き方を身につける
はじめに
前回は基本的なモックの使い方について学習しました。今回は、より信頼性の高いテストを実現するinstance_doubleについて学んでいきます。
前回のモックテストにも課題があります。それは実際のクラスに存在しないメソッドでもテストが通ってしまう点です。この課題を解決するためのテクニックを身につけましょう。
instance_doubleとは
instance_doubleは、特定のクラスに基づいてモックを作成する機能です。クラスの仕様に準拠したテストを強制することで、テストの信頼性を高めることができます。
主な特徴として、クラスで定義されていないメソッドや不適切な引数を設定しようとすると、即座にエラーが発生します。これにより、リファクタリング時の仕様変更を早期に検知することが可能となります。
通常のdoubleでは実装とテストの間に齟齬があっても気づきにくいですが、instance_doubleを使うとそのような問題を素早く発見できます。
通常のdoubleの課題
まずは、前回学習したメール送信の例を使って、通常のdoubleの課題を確認していきましょう。メール送信を担当するEmailServiceクラスのコードは以下の通りです。
class EmailService  def send_welcome_mail(email)    # 実際のメール送信処理(今回は省略)    puts "Welcome mail sent to #{email}"  endendEmailServiceを利用するUserRegisterクラスは以下のようになっています。
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)  endendここで、EmailServiceのメソッド名をsend_welcome_mailからdeliver_welcome_mailに変更してみましょう。実際の開発現場では、仕様変更やリファクタリングでこのようなことがよく起こります。
class EmailService  def deliver_welcome_mail(email)    # 実際のメール送信処理(今回は省略)    puts "Welcome mail sent to #{email}"  endend通常のdoubleを使用したテストコードを実行してみます。
require_relative '../user_register'
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  endendこのテストは成功してしまいますが、これだと問題です。実際のEmailServiceクラスではメソッド名が変更されているにもかかわらず、テストでは古いメソッド名のままでも通過してしまうためです。
そのため、たとえばこのままアプリをデプロイしてしまうと、実際の動作時にはメソッドが見つからずエラーが発生してしまいます。これは、テストが正常に通ったにもかかわらず、本番環境で問題が発生するという最悪のシナリオです。
instance_doubleを使用した改善
この問題を解決するために、instance_doubleを使ってテストを書き直してみましょう。
RSpec.describe UserRegister do  describe '#register' do    it 'ウェルカムメールが送信される' do      # EmailServiceクラスに基づいてモックを作成      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  endend通常のdoubleとinstance_doubleの違いは、後者が実際のクラスに基づいて作成される点です。これにより、実際のクラスに存在しないメソッドを使おうとするとエラーが発生します。
このテストを実行すると、以下のようなエラーが表示されます。
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このエラーは、send_welcome_mailメソッドがEmailServiceクラスに存在しないことを示しています。これにより、実装との不整合を早期に発見することができます。
正しい実装への修正
エラーを解消するために、UserRegisterクラスとテストコードを新しいメソッド名に合わせて修正します。
まず、UserRegisterクラスを修正します。
class UserRegister  def initialize(email_service)    @email_service = email_service  end
  def register(email)    puts 'ユーザー登録中...'    @email_service.deliver_welcome_mail(email)  endendテストコードも同様に修正します。
RSpec.describe UserRegister do  describe '#register' do    it 'ウェルカムメールが送信される' do      email_service = instance_double(EmailService)      expect(email_service).to receive(:deliver_welcome_mail).with('test@example.com')
      user_register = UserRegister.new(email_service)      user_register.register('test@example.com')    end  endendこれで、実装とテストコードが一致し、信頼性の高いテストになりました。このように、instance_doubleを使うことで、実装とテストの間の不整合を早期に発見することができます。
instance_doubleの活用シーン
ここまでで、instance_doubleの基本的な使い方とその利点について学びました。次に、実際の開発現場でどのように活用できるかを考えてみましょう。
リファクタリング時の安全性確保
コードをリファクタリングする際、メソッド名やパラメータの変更はよく行われます。instance_doubleを使うことで、そのような変更に対するテストの信頼性を確保できます。変更後のコードとテストコードの間に不整合があれば、すぐにエラーが発生するため、問題を早期に発見できます。
言うなれば、instance_doubleはリファクタリングの「セーフティネット」として機能します。これにより、開発者は安心してコードを変更できるようになります。
チーム開発での一貫性維持
複数の開発者が同時に作業する場合、誰かがクラスのインターフェースを変更すると、他の開発者のテストコードに影響を与える可能性があります。instance_doubleを使うことで、そのような変更を即座に検知でき、チーム全体での一貫性を維持しやすくなります。
これが、doubleとinstance_doubleの大きな違いです。doubleは自由度が高い分、実装との不整合を見逃しやすくなりますが、instance_doubleはクラスに基づいているため、そのような問題を防ぐことができます。
まとめ
本章では、instance_doubleの重要な特徴について学習しました。
実際のクラスに存在しないメソッドや不適切な引数を使用しようとすると、即座にエラーが発生します。これにより、リファクタリングによる仕様変更を早い段階で検知することができます。
実装とテストコードの間の不整合を防ぎ、より堅牢なテストを実現できるため、長期的なメンテナンス性の向上にもつながります。
次の章では、instance_doubleを使ったより実践的なテスト手法について学習していきます。メソッドの返り値の設定方法や、複数のメソッド呼び出しのテスト方法など、より高度な使い方を身につけていきましょう。
Basicプランでより詳しく学習
この先のコンテンツを読むにはBasicプラン以上が必要です。より詳細な解説、実践的なサンプルコード、演習問題にアクセスして学習を深めましょう。