instance_doubleでメソッドの返り値もテストしよう
- 学習の目標
- はじめに
- 在庫管理システムの実装例
- 購入管理クラスの実装
- テストコードの実装
- 効率的なテストコードの記述方法
- さらなる改善: subjectの活用
- instance_doubleの返り値設定のバリエーション
- まとめ
学習の目標
本章では、以下の内容を学習します。
- instance_doubleでメソッドの返り値を設定する方法を理解する
- 複数のテストケースを効率的に管理する技術を習得する
- RSpecのlet、subject、contextを活用したテストコードの書き方を学ぶ
- 実践的な例を通じてテストコードの改善方法を身につける
はじめに
前回までは、instance_doubleを使用して実装と一致しないメソッド名や引数を指定した時にエラーになることを確認しました。これにより、テストと実装の間に不整合があった場合に早期に発見できることがわかりました。
本章では、instance_doubleでモックを作成した際のメソッドの返り値の設定方法について学習します。実際のシステム開発では、他のクラスから返される値に応じて処理を変える場面が多くあります。そのようなケースでのテスト方法を、具体的な例を通して見ていきましょう。
在庫管理システムの実装例
返り値の設定について理解を深めるため、商品の在庫を管理するシステムを例として実装していきます。このような在庫管理システムは、ECサイトやPOSシステムなど、実際の業務システムでよく見られるものです。
まずは、在庫データベースと通信するための基本的なクラスを作成します。stock_service.rbというファイルを作成し、以下のコードを実装してください。
class StockService  def fetch_stock_quantity(product_id)    # データベースから在庫数を取得する処理(今回は省略)    # 実際のシステムでは、ここでデータベースに接続して在庫数を取得する    42  endendこのクラスについて少し詳しく説明しましょう。StockServiceクラスは、商品の在庫情報を管理するデータベースにアクセスする役割を持っています。fetch_stock_quantityメソッドは商品IDを引数に取り、対応する商品の在庫数を返す機能を提供します。実際の開発では、このメソッド内でデータベースにアクセスして在庫数を取得しますが、今回は説明を簡略化するため、常に42を返すようにしています。
購入管理クラスの実装
次に、在庫のチェックと商品の購入を管理するクラスを作成します。このクラスは、先ほど作成したStockServiceクラスを使って在庫を確認し、購入が可能かどうかを判断します。
purchase_manager.rbというファイルを作成し、以下のコードを実装してください。
require_relative './stock_service'
class PurchaseManager  def initialize(stock_service)    @stock_service = stock_service  end
  def purchase(product_id, quantity)    stock = @stock_service.fetch_stock_quantity(product_id)    if stock >= quantity      # 購入処理(今回は省略)      true    else      false    end  endendPurchaseManagerクラスは、商品の購入処理を担当します。ここでは「依存性の注入」というパターンを採用し、コンストラクタでStockServiceのインスタンスを受け取ります。このアプローチにより、テスト時にモックオブジェクトを差し込むことが容易になります。
purchaseメソッドは商品IDと購入希望数量を受け取り、在庫の確認を行います。StockServiceから取得した在庫数と比較し、在庫が十分にある場合は購入処理を行ってtrueを返し、不足している場合はfalseを返します。実際のシステムでは、在庫の減少や注文データの作成なども行いますが、ここでは省略しています。
テストコードの実装
次に、このPurchaseManagerクラスのテストを実装していきます。ここでは、instance_doubleを使用してStockServiceのモックを作成し、異なる在庫状況をシミュレートしてテストを行います。
spec/purchase_manager_spec.rbというファイルを作成し、以下のコードを記述してください。
require_relative '../purchase_manager'
RSpec.describe PurchaseManager do  describe '#purchase' do    it '在庫が十分にある場合は購入に成功する' do      stock_service = instance_double(StockService)      allow(stock_service).to receive(:fetch_stock_quantity).with(123).and_return(50)
      purchase_manager = PurchaseManager.new(stock_service)      result = purchase_manager.purchase(123, 30)
      expect(result).to be true    end  endendこのテストコードでは、instance_doubleを使ってStockServiceのモックオブジェクトを作成しています。そして、allowメソッドを使って、商品ID 123の在庫数として50を返すように設定しています。これにより、実際のデータベースにアクセスすることなくテストを実行できます。
テストでは、30個の商品を購入しようとしていますが、在庫数は50あるため、購入は成功するはずです。そのため、purchaseメソッドの戻り値がtrueであることを検証しています。
効率的なテストコードの記述方法
先ほどのテストコードは基本的な実装ですが、RSpecの機能を活用することでより効率的に記述することができます。特に複数のテストケースがある場合に有効です。
まず、letを使用して、テストで使用する値を管理する方法を見ていきましょう。
require_relative '../purchase_manager'
RSpec.describe PurchaseManager do  describe '#purchase' do    let(:product_id) { 123 }    let(:quantity) { 30 }    let(:stock_quantity) { 50 }    let(:stock_service) {      instance_double(        StockService,        fetch_stock_quantity: stock_quantity      )    }
    it '在庫が十分にある場合は購入に成功する' do      purchase_manager = PurchaseManager.new(stock_service)      result = purchase_manager.purchase(product_id, quantity)      expect(result).to be true    end  endendletを使うことで、テストで使用する値が一箇所にまとまり、管理が容易になります。特に複数のテストケースで同じ値を使う場合に効果的です。値の変更が必要になっても、定義箇所のみを修正すれば良いので、コードの重複も防げます。
また、instance_doubleの定義方法も少し変わっています。instance_doubleの第2引数以降で、メソッド名とその戻り値をハッシュ形式で指定することができます。これにより、allow(...).to receive(...).and_return(...)と書く必要がなく、よりシンプルにモックの振る舞いを定義できます。
さらなる改善: subjectの活用
次に、subjectを使用してテストをより簡潔に記述する方法を見ていきましょう。
RSpec.describe PurchaseManager do  describe '#purchase' do    let(:product_id) { 123 }    let(:quantity) { 30 }    let(:stock_quantity) { 50 }    let(:stock_service) {      instance_double(        StockService,        fetch_stock_quantity: stock_quantity      )    }
    subject { PurchaseManager.new(stock_service).purchase(product_id, quantity) }
    context '在庫が十分にある場合' do      it '購入に成功する' do        expect(subject).to be true      end    end
    context '在庫が不足している場合' do      let(:stock_quantity) { 29 }
      it '購入に失敗する' do        expect(subject).to be false      end    end  endendsubjectを使うことで、テストの対象となる処理を明確に定義できます。上記の例では、「PurchaseManagerのインスタンスを作成し、purchaseメソッドを呼び出す」という処理をsubjectとして定義しています。これにより、各テストケースでは単にsubjectの結果を検証するだけで済むようになり、コードがよりシンプルになります。
また、contextを使って、異なるテストシナリオを明確に分けています。在庫が十分にある場合と不足している場合という2つのシナリオを明示的に区別することで、テストコードの意図が伝わりやすくなります。
在庫不足のケースでは、stock_quantityの値を上書きして異なる状況をテストしています。letで定義された値は、そのブロック内で再定義することができるので、必要な部分だけを変更してテストケースを追加できます。これにより、テストケース間の違いが明確になり、コードの重複も避けられます。
instance_doubleの返り値設定のバリエーション
instance_doubleでメソッドの返り値を設定する方法にはいくつかのバリエーションがあります。状況に応じて適切な方法を選ぶと良いでしょう。
基本的な返り値設定
# 方法1: allowメソッドを使うstock_service = instance_double(StockService)allow(stock_service).to receive(:fetch_stock_quantity).and_return(50)
# 方法2: initialize時にハッシュで指定stock_service = instance_double(StockService, fetch_stock_quantity: 50)引数に応じて異なる値を返す
stock_service = instance_double(StockService)allow(stock_service).to receive(:fetch_stock_quantity).with(123).and_return(50)allow(stock_service).to receive(:fetch_stock_quantity).with(456).and_return(10)この例では、商品ID 123の場合は在庫数50を、商品ID 456の場合は在庫数10を返すように設定しています。これにより、異なる商品に対する処理を1つのテストでカバーできます。
連続して異なる値を返す
stock_service = instance_double(StockService)allow(stock_service).to receive(:fetch_stock_quantity).and_return(50, 40, 30)この例では、fetch_stock_quantityメソッドが呼ばれるたびに異なる値を返します。1回目は50、2回目は40、3回目は30というように連続して値が変わります。在庫の減少など、状態が変化する状況をテストする際に有用です。
まとめ
本章では、instance_doubleを使用してメソッドの返り値を設定する方法について学習しました。実際のデータベースアクセスなど、外部依存のある処理をシミュレートすることで、テストの実行速度向上と安定性の確保が可能になります。
また、RSpecのlet、subject、contextなどの機能を活用することで、テストコードをより効率的に管理する方法も学びました。これらの機能を使いこなすことで、テストコードの可読性が向上し、メンテナンスも容易になります。
次の章では、スタブとモックの違いや使い分けについて、より詳しく学んでいきましょう。それぞれの特徴を理解し、適切なシーンで活用できるようになることが、効果的なテスト設計の鍵となります。
Basicプランでより詳しく学習
この先のコンテンツを読むにはBasicプラン以上が必要です。より詳細な解説、実践的なサンプルコード、演習問題にアクセスして学習を深めましょう。