😎

RSpecのモック(Mock) とstubの使い方

2024/03/25に公開

RSpecのMockとは

モック(Mock)は、RSpecで使用される概念の1つで、テスト中に特定のメソッドが呼び出されたかどうかを追跡したり、テスト中に期待される振る舞いや他のコードとのやり取りをシミュレートするために使い、
他のオブジェクトやクラスを代替することができるオブジェクトです。
テスト中に、テスト対象のコードが他のクラスやオブジェクトとやり取りする際に、それらの依存関係を置き換えるために使われます。

ex)
外部のサービスやデータベースとの通信をテストする場合
実際の通信を待たずにモックを使ってテストでき、テストが速くて便利になります。

テストを行うときに、本物のものを使うのが難しい場合やめんどうな場合に、似たような振る舞いをするものモックです。

これだとわかりづらいと思うので、小学生にもわかるように説明します。

小学生にもわかる説明

小学生にもわかるような説明をします。ChatGPT参照。
ex)
料理をするゲームをしていると想像してみてください。あなたはシェフで、材料を欲しいときに友達に頼むことができます。時々、あなたは友達が正しい材料を提供してくれるかどうかを確認したいことがあります。

そこで、あなたは「シェフの友達のふりをするお友達」という架空の友達を作ります。
この架空の友達は、あなたが何を欲しがるかを知っていて、いつもあなたが頼むときに材料を提供してくれます。これによって、ゲームを遊ぶうちに本当の友達にいちいち頼まなくても済むのです。

このゲームの中で、架空の友達に材料を頼むことは、実際に友達に頼んだ場合に正しい材料をもらえるかどうかをテストしていることになります。
そして、もし架空の友達が正しい材料を提供してくれたら、本当の友達も同じようにしてくれるとわかります。

つまり、RSpecのモックは、あなたの架空の友達のようなものです。
テスト中にプログラム(シェフ)が他の部分と正しくやり取りしているかをテストするのに役立ちますが、テスト中に実際にそれらを使うことなく行います。これは、テストのためだけに実物を使わずに本番環境を再現することができるということです。

mockはallowexpectといったメソッドを使用して定義されます。
具体的なオブジェクトの振る舞いを定義することで、テスト中でその振る舞いを再現できます。

これはshowアクションが、Admins::Users::ShowServiceから返されたデータを適切に割り当てるかどうかをテストしています。

require 'rails_helper'

RSpec.describe Admins::Users::ShowService, type: :service do
  let!(:user) { create(:user) }
  #中略
  describe '#call' do
    # subjectにdescribed_class.call(user: user)を設定し、#callメソッドの実行結果を取得
    subject { described_class.call(user: user) }

    context '正常系' do
    #  請求書の情報を取得するクラスのモックを作成
    #  mock_invoiceとinvoices_responseを定義
    #  取得する時の条件などは、Invoicesクラスでテスト
    # 実際の請求書情報の取得をシミュレート
      let(:mock_invoice) { instance_double("Invoices") } 
      let(:invoices_response) { ["invoice1", "invoice2"] }

      before do
        allow(Invoices).to receive(:new).and_return(mock_invoice)
        allow(mock_invoice).to receive(:billed_invoices).and_return(invoices_response)
      end

    # メソッドの呼び出しに成功したことを検証
      it 'true を返す' do
        expect(subject[:success]).to eq(true)
      end

      it "data[:ticket_number_per_shop] が正しくセットされる" do
     
           # Admins::Users::ShowServiceのモックを作成し、適切なデータが返されるように設定
        allow(described_class).to receive(:call).with(user: user).and_return(success: true, data: { ticket_number_per_shop: "tickets_response" })
      # アクションが成功した場合、data[:ticket_number_per_shop] が期待されるデータと一致することを期待
        expect(subject[:data][:ticket_number_per_shop]).to eq("tickets_response")
      end
#中略
    end

    context '異常系' do
      context '処理の中で例外が発生した時' do
        let(:instance) { described_class.new(user: user) }

        before do
          allow(instance).to receive(:set_owner).and_raise(StandardError)
        end

        it '例外が通知される' do
          expect(ExceptionNotifier).to receive(:notify_exception).once
          instance.template_method
        end
      end
    end
  end
end

このテストの解説(モックを中心に)

テストの中で使われている「モック」という言葉は、テストを行う際に本物のものの代わりに使う架空のもので、実際の請求書情報を取得するクラスであるInvoicesクラスを再現したり、例外が発生したときに通知を受け取るExceptionNotifierクラスの振る舞いを模倣したりすることができます。

ex)
「mock_invoice」というモックは、実際の請求書情報を取得するInvoicesクラスの代わりに使われます。
これは、テストの中で請求書情報を取得するプロセスをシミュレートするために使われます。
そして、実際のデータベースにアクセスせずに、テストを行うことができます。

また、「例外が通知される」という部分も、例外が発生したときに通知を受け取る機能をモックしています。
これにより、例外が発生したときに適切なアクションが実行されることを確認することができます。

モックはテストを行う際に必要な機能や振る舞いを模倣するための道具であり、
テストをシンプルかつ効果的に行うのに役立ちます。

mockのコードの解説

以下のコードの説明をしていきます。

before do
 allow(Invoices).to receive(:new).and_return(mock_invoice)
 allow(mock_invoice).to receive(:billed_invoices).and_return(invoices_response)
end

beforeブロック内のコードは、テストケースの各example(itブロック内のテスト)が実行される前に実行される設定を行います。具体的には、Admins::Users::ShowServiceクラスの#callメソッド内で使われるInvoicesクラスやそのインスタンスをモック化します。

allow(Invoices).to receive(:new).and_return(mock_invoice)

Invoicesクラスのnewメソッドをモック化しています。つまり、実際にはInvoicesクラスの新しいインスタンスを作成せず、代わりにmock_invoiceと呼ばれるモックインスタンスを返します。
allowメソッドは、RSpecでモックやスタブを設定するためのメソッドです。ここでは、Invoicesクラスのnewメソッドが呼び出されたときに、代わりにmock_invoiceを返すように設定しています。
allow(mock_invoice).to receive(:billed_invoices).and_return(invoices_response)

mock_invoiceというモックインスタンスのbilled_invoicesメソッドをモック化しています。つまり、実際にはbilled_invoicesメソッドの本来の実装を実行せず、代わりにinvoices_responseという配列を返します。
ここでもallowメソッドを使用して、mock_invoiceのbilled_invoicesメソッドが呼び出されたときに、invoices_responseを返すように設定しています。
このように設定することで、#callメソッドがInvoicesクラスやそのインスタンスを使って請求書情報を取得する部分をテストする際に、本物のデータベースや外部システムへのアクセスを避け、代わりにモック化されたデータを使用してテストを行うことができます。これにより、テストの安定性が向上し、テストの速度も向上します。

mockのコード解説2

以下のコードを解説していきます。

before do
 allow(instance).to receive(:set_owner).and_raise(StandardError)
end

it '例外が通知される' do
 expect(ExceptionNotifier).to receive(:notify_exception).once
          instance.template_method
end

beforeブロック内のallow(instance).to receive(:set_owner).and_raise(StandardError):

allowメソッドを使用して、instanceオブジェクトのset_ownerメソッドをモック化しています。つまり、実際にset_ownerメソッドが呼び出されたときには、代わりに指定した例外(StandardError)が発生します。
itブロック内のexpect(ExceptionNotifier).to receive(:notify_exception).once:

expectメソッドを使用して、ExceptionNotifierのnotify_exceptionメソッドが呼び出されることを期待しています。そして、.onceというメソッドを使用して、一度だけ呼び出されることを検証しています。
instance.template_method:

instanceオブジェクトのtemplate_methodメソッドが呼び出されます。このメソッド内でset_ownerメソッドが呼び出され、その中でStandardErrorが発生することが期待されています。
つまり、このテストケースでは、set_ownerメソッド内で例外が発生した場合に、その例外が正しく通知されるかどうかを検証しています。これにより、アプリケーションが想定外のエラーに対して適切にハンドリングされることを確認することができます。

テストの修正点

allow(described_class).to receive(:call).with(user: user)を使用していますが、これはテスト中にメソッドをモックしようとしているようです。このアプローチは通常不要で、
正常系のテストで、#callメソッド内で他のメソッドを呼び出している場合は、それらのメソッドが適切に動作することを検証すべきですが、自分のメソッドをモックする必要はありません。
修正すると以下のようになります。

it "data[:ticket_number_per_shop] が正しくセットされる" do
  # `described_class.call`を呼び出してもう一度モックを作成する必要はありません。
  expect(subject[:data][:ticket_number_per_shop]).to eq("tickets_response")
end

モックを使う場合

外部リソースへのアクセスを制御する場合。
他のオブジェクトやクラスとの相互作用をテストする場合。
非同期処理をテストする場合。
依存関係の解決を容易にする場合。
これらの場面でRSpecのモックを使うことで、テストを効率的かつ信頼性高く実行できます。

stub

RSpecのstubは、テスト中に本物のオブジェクトやメソッドの振る舞いを代わりにする道具です。イメージとしては、本物の動物が登場する予定だったけれど、その動物が来なかったときに、紙やぬいぐるみなどの代わりのものを使うようなものです。

たとえば、学校でクイズを出すことを考えてみましょう。普通は先生が質問をしますが、先生がいない日や、先生が別の用事で忙しい場合は、クラスメートが先生の代わりに質問をします。ここで、クラスメートが先生の代わりに質問をするのがRSpecのstubの役割です。

つまり、RSpecのstubを使うと、テストの中で本物のオブジェクトやメソッドの代わりに使えるフェイク(偽物)を作ることができます。これにより、テストが外部の状況に影響されずに実行できるようになります。

# 仮想のクラスを定義します(実際のコードでは不要です)
class Calculator
  def self.add(a, b)
    a + b
  end
end
RSpec.describe Calculator do
  describe '.add' do
    it 'adds two numbers' do
      # Calculator.addメソッドを呼び出す際に、引数が3と5の場合に10を返すように設定します
      allow(Calculator).to receive(:add).with(3, 5).and_return(10)
      
      # Calculator.addメソッドを呼び出します
      result = Calculator.add(3, 5)
      
      # 戻り値が10であることを検証します
      expect(result).to eq(10)
    end
  end
end

このコードでは、Calculatorクラスのaddメソッドが呼び出されたときに、引数が3と5の場合に10を返すようにstubを設定しています。そして、実際にCalculator.addメソッドを呼び出し、その結果を検証しています。

このように、RSpecのstubを使用することで、テスト中に特定のメソッドの振る舞いを制御し、テストの実行結果を予測可能にすることができます。

有益資料

https://qiita.com/jnchito/items/640f17e124ab263a54dd

https://zenn.dev/nogtk/articles/42d672aa85c80a6a886c

Discussion