ActiveRecord::RecordInvalid が発生するかどうかをテストするために ActiveModel::Errors のモックを作りたい。
実行環境
ActiveRecord::RecordInvalid が発生するかどうかのテスト
subjectが実行された際 ActiveRecord::RecordInvalid が発生するかどうかのテストコードを実装します。
なお、subjectは省略していますが、subjectで定義しているものには、user.save! の処理が含まれています。
# Userのインスタンスに対してsave!メソッド実行時にActiveRecord::RecordInvalidがraiseされるようにスタブ化 before { allow_any_instance_of(User).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) } it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid) }
Model にerrors や class を定義するスタブを作成
つぎに user.save! に失敗した場合、以下のようにエラーメッセージをログに出力するコードが書かれているとします。
Rails.logger.error e.message
その場合に、subjectを実行し ActiveRecord::RecordInvalid が発生した時に、エラーログが出力されるかどうかのテストコードを実装します。
まずテストの前処理としてスタブの定義を行います。
let(:errored_model) do instance_double( User, errors: instance_double('errors', full_messages: ['nameを入力してください'], messages: { name: ['を入力してください'] }), class: class_double(User, i18n_scope: :activerecord), ) end before do # Userのインスタンスに対してsave!メソッドが実行された時にActiveRecord::RecordInvalidがraiseされるようにスタブ化 allow_any_instance_of(User).to receive(:save!).and_raise(ActiveRecord::RecordInvalid, errored_model) # Rails.loggerのオブジェクトに対してerrorメソッドを呼び出せるようにする allow(Rails.logger).to receive(:error) end
and_raise について
rspec-mocks/lib/rspec/mocks/message_expectation.rb at main · rspec/rspec-mocks · GitHub
こちらにあるように and_raise 内で、raise メソッドが呼び出されているため、raise ActiveRecord::RecordInvalid, user
とするように、
and_raise の引数に ActiveRecord::RecordInvalid, user
を渡してあげるとよさそうです。
instance_double の errors や class について
User の InstanceDouble で errors を指定していないと以下のようなエラーが発生します。
RSpec::Mocks::MockExpectationError: #<InstanceDouble(User) (anonymous)> received unexpected message :errors with (no args) from /app/.bundle/ruby/2.6.0/gems/rspec-support-3.10.2/lib/rspec/support.rb:102:in `block in <module:Support>'
ほかにも i18n_scope メソッドが定義されていないと以下のようなエラーが発生します。
NoMethodError: undefined method `i18n_scope' for RSpec::Mocks::InstanceVerifyingDouble:Class from /app/.bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/validations.rb:22:in `initialize'
内部的に ActiveRecord において i18n_scope メソッドを呼び出しているようでした。
rails/activerecord/lib/active_record/translation.rb at main · rails/rails · GitHub
最終的なテストコード
let(:errored_model) do instance_double( User, errors: instance_double('errors', full_messages: ['nameを入力してください'], messages: { name: ['を入力してください'] }), class: class_double(User, i18n_scope: :activerecord), ) end before do allow_any_instance_of(User).to receive(:save!).and_raise(ActiveRecord::RecordInvalid, errored_model) allow(Rails.logger).to receive(:error) end # 実際にRails.logger.errorが呼び出され、エラーメッセージが引数として渡されることをモックで確認する it 'エラーログが出力されること' do subject expect(Rails.logger).to have_received(:error).with("バリデーションに失敗しました: nameを入力してください") end
モック/スタブ実装のためのメソッド
上記で使用したメソッドを整理します。
そもそも RSpec におけるスタブとモックの違い
スタブは、他のメソッドが実行されないようにして欲しい値を返させるものです。
つまり、テストで確認したい内容のために何か都合のいい値を返すように設定したものをスタブと言います。
モックは、指定のメソッドが実行されたかどうかを、引数や頻度を指定してチェックするものです。
instance_double
特定のクラスに対して double(身代わり)を作成します。
指定したクラスで定義されているインスタンスメソッドのみが、スタブとして許可されます。
ほとんどdoubleと同様だが、allowなどで定義するメソッドが、引数で指定したクラス(今回の User)のインスタンスメソッドとして存在していないとテストが失敗します。
class_double
実際のクラスの情報と紐付いて実際のクラスに実装されていないメソッドのスタブ化はできないようになります。
ほとんどinstance_doubleと同様だが、こちらはクラスメソッドに対して適用されます。
allow_any_instance_of
生成されたインスタンスすべてに対して、スタブを設定できます。