Rspec モック・スタブ
モックの作成
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