👌

Rspec モック・スタブ

2024/10/12に公開

モックの作成

doubleメソッドを使ってモックを作成します。

RSpec.describe "A mock example" do
  it "creates a mock object" do
    user = double("User") #モックを作成
  end
end

モックとは只のオブジェクトです。上記のコードではdouble("User")と記述されていますがUserクラスとは何の関係もなく、只のUserと名付けられたオブジェクトです。
このUserモックに挙動を定義すること(=スタブ化)もできます。

user = double("User")
allow(user).to receive(:name).and_return("太郎")

上記のようにスタブ化することでUserモックに対してnameメソッドが実行された時に「太郎」と返却するように振る舞いを定義できます。

では実際の使い方を見てみます。

例えば下記のようなOrderProcessorという名前のクラスがあったとします。
callメソッド内部ではインスタンス化したOrderApiClientを返すorder_api_clientが呼び出されています。この状態はOrderProcessorクラスはOrderApiClientクラスに依存していると言える状態です。

class OrderProcessor
  def initialize(user:, ammount:, processor_id:, token:)
    @user = user
    @ammount = ammount
    @processor_id = processor_id
    @token = token
  end

  def call
    order_api_client.post(processor_id: @processor_id, amount: @amount)
  end

  def order_api_client
    @order_api_client ||= OrderApiClient.new(token: @token, user: @user)
  end
end

次にOrderProcessorクラスの挙動をテストするテストクラスを書いていきます。

require 'rails_helper'

RSpec.describe OrderProcessor do
  let(:user) { double('User') }  # モックとしてユーザーオブジェクトを作成
  let(:ammount) { 1000 }         # 金額
  let(:processor_id) { '12345' } # プロセッサID
  let(:token) { 'token_abc123' } # トークン

  # subjectとしてOrderProcessorのインスタンスを定義
  subject { described_class.new(user: user, ammount: ammount, processor_id: processor_id, token: token) }

  # モック化したOrderApiClientのレスポンスを格納
  let(:response_data) { { success: true, order_id: 'order_123' } }

  before do
    # OrderApiClientのスタブを作成し、newメソッドの振る舞いを定義
    order_api_client = double('OrderApiClient')
    allow(OrderApiClient).to receive(:new).with(token: token, user: user).and_return(order_api_client)

    # order_api_clientのpostメソッドが呼ばれたときにレスポンスデータを返す
    allow(order_api_client).to receive(:post).with(processor_id: processor_id, amount: ammount).and_return(response_data)
  end

  describe '#call' do
    it 'returns the correct response from order_api_client' do
      # テスト対象のメソッドを実行し、結果を受け取る
      result = subject.call

      # order_api_clientのpostメソッドが正しい引数で呼ばれたことを確認
      expect(OrderApiClient).to have_received(:new).with(token: token, user: user)
      expect_any_instance_of(OrderApiClient).to have_received(:post).with(processor_id: processor_id, amount: ammount)

      # 結果が期待するレスポンスデータと一致することを確認
      expect(result).to eq(response_data)
    end
  end
end

beforeブロックのorder_api_client = double('OrderApiClient')部分でモックを定義して後続部分でそのモックに対してpostメソッドが決まった引数で呼ばれた時の振る舞いを定義しています。(=スタブ化)

テストケースでは下記部分で引数のテストを行っています。(正直、業務ではあまり書いたことがない。)

# order_api_clientのpostメソッドが正しい引数で呼ばれたことを確認
expect(OrderApiClient).to have_received(:new).with(token: token, user: user)
expect_any_instance_of(OrderApiClient).to have_received(:post).with(processor_id: processor_id, amount: ammount)

最終的にはモック化したクラスが返却してきたレスポンスを検証しています。

モックの利点

1. 外部依存を排除できる

モックを使うことで、外部システムやクラス、APIに依存せずにテストを実行できます。
: 支払い処理クラスのテストで、実際の決済APIを呼ばずにモックを使うことで、テストを安全かつ迅速に実行。

2. テスト対象のクラスに集中できる

モックを使うことで、テスト対象のクラスやメソッドのみに集中したテストが可能になります。テストが他のクラスやサービスに影響されないため、テストがシンプルで明確になります。

3. 予測可能な動作を定義できる

モックを使って、特定のメソッドに対する予測可能な返り値を設定できます。APIが成功した場合と失敗した場合、両方のシナリオをモックで容易にテストできます。たとえば、APIがタイムアウトしたり、特定のエラーメッセージを返すシナリオも再現できます。

モックが有効なケースの例

  • 外部API: 外部APIにリクエストを送る処理をテストする際に、APIクライアントをモックすることで、実際にAPIを呼び出さずにテストできます。
  • データベース操作: 大量のデータを扱う場合や、データベース接続を必要としないテストでは、データベースアクセスをモックすることでテストが迅速になります。
  • 非同期処理: 時間のかかる非同期処理(例:バッチ処理やキュー処理)をモックで短時間にシミュレートしてテスト可能。

Discussion