zenet_logo

-株式会社ゼネット技術ブログ-

Rspecで、条件に応じて返す値を変えたり例外を発生させたりするモックを作成する

f:id:zenet-tech:20210317102008p:plain

お疲れ様です、ゼネットの坂本です。

今回は、Rspecを作成していて詰まった点があるので
備忘録・情報共有を兼ねて皆さんにご紹介したいと思います。


■前提

例えば、以下のようなモデルがあると仮定します。

  • User:ユーザに関するモデル。(UserAddressと1対1の関係)
  • UserAddress:ユーザの住所に関するモデル。(Userと1対1の関係)

また、その前提で以下のようなジョブがあると仮定します。

require 'workers/base_worker'

module Job
  class UpdateUserInfo < BaseWorker
    def self.execute
      User.find_each(batch_size: 300) do |user|
        user_address = UserAddress.find_or_initialize_by(user_id: user.id)
        # 更新処理
      rescue StandardError => e
        # エラー時の処理
      end

      # 終了後の処理
    end
  end
end

上記のJob::UpdateUserInfo.executeを大まかに説明すると、以下の通りです。

  • ユーザ1人1人に対して、ユーザに対する住所のデータを検索または初期化し、住所のデータに対して更新処理を行う。
  • もし特定のユーザで例外が発生しても、rescueによる例外処理が行われそのまま処理を続行する。
    例外が発生したユーザ以外は更新される

■前提を元に、書きたかったRspecの内容

このバッチ処理に対して、「ある任意のユーザに対して更新処理を行なっている際にエラーが発生した場合でも、
それ以外のユーザに対しては正常に処理が行われること
」を確認するRspecを書きたいと考えました。
そのため、特定の条件でエラーを意図的に発生されるモックの作成を行う必要があったので以下のようにしたいと考えました。

  • UserAddress.find_or_initialize_byを行なって……
    • 特定のuser_idが渡されたら、既存のUserAddressを返す
    • 特定のuser_idが渡されたら、StandardErrorを発生させる
    • それ以外のuser_idが渡されたら、新しいUserAddressを返す

当初は条件に応じて挙動が変わるモックの作り方が分からず苦戦していましたが、
調査・試行錯誤の結果、以下のような実装で実現することが出来ました。

■Rspec

require 'rails_helper'
require 'kconv'

describe Job::UpdateUserInfo do
  describe '#run' do
    let!(:user1) { create(:user) }
    let!(:user2) { create(:user) }
    let!(:user3) { create(:user) }
    let!(:user1_address) { create(:user_address, user_id: user1.id) }

    context '実行中にエラーが発生した場合' do
      it 'エラーが発生したユーザ以外は、正常にデータが更新される' do
        allow(UserAddress).to receive(:find_or_initialize_by) do |arg|
          case arg[:user_id]
          when user1.id
            user1_address
          when user2.id
            raise StandardError
          else
            UserAddress.new(user_id: arg[:user_id])
          end
        end

        ## 以下、テスト内容

      end
    end

    ## 省略

  end
end

ポイントは、以下の通りallow(MODEL).to receive(:METHOD)のようなモックを定義する際に
ブロック変数を渡しているところです。

allow(UserAddress).to receive(:find_or_initialize_by) do |arg|

これを利用すると、メソッドに対して与えた引数を取得することが可能で、
引数に応じて何を返すか?何の例外を発生させるか?を分岐させることが可能となります。

もう少し細かく説明すると、以下の通りになります。

## Job側 ##
# 「user.id = 1」だと想定
user_address = UserAddress.find_or_initialize_by(user_id: user.id)


## Rspec側 ##
# UserAddress.find_or_initialize_byがJob側で呼ばれたので、
# この処理が適用される。
allow(UserAddress).to receive(:find_or_initialize_by) do |arg|
  # この時、"arg"はJob側でメソッド「find_or_initialize_by」で与えた引数で
  # {:user_id => 1}  になっている。
  # そのため "arg[:user_id]" で、1が取得出来る。
  case arg[:user_id]
  when user1.id
    # user1のidだったら、作成されていたuser_addressのデータをそのまま返す(find扱いにする)
    user1_address
  when user2.id
    # user2のidだったら、StandardErrorを発生させる
    raise StandardError
  else
    # それ以外だったら、新しいUserAddressを返す(initialize扱いにする)
    UserAddress.new(user_id: arg[:user_id])
  end
end

こうすることで、特定条件化でエラーを発生させて、
それ以外の場合はFindやInitialize扱いにするようなモックを作成することが出来ました。


以上、Rspecで条件に応じて返す値を変えたり、例外を発生させたりするモックを作成する方法でした。
同じような内容で困っている方のヒントになれば幸いです。