こんにちは、福岡開発拠点の小林です。 マネーフォワード クラウド経費 と マネーフォワード クラウド債務支払 の開発を担当しています。
この記事は Money Forward Engineering 1 Advent Calendar 2022 の15日目の記事です。
今回はみなさん大好き rspec-rails でテストコードを書くときに知ってると役立つかもしれない tips について一つご紹介します。
本題:allow().to receive() にはブロックを渡せる
下のようなサンプルコードがあるとします。
class User include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Validations::Callbacks attribute :name, :string def convert_name self.name = name.upcase end end
この中で convert_name
のようなメソッドをモックして別の処理にしたい場合、 allow().to receive() {}
の ブロック渡し で実現することができます。
例えば、upcase
ではなく downcase
にしてみたいとき。
user = User.new(name: 'sAmplE') allow(user).to receive(:convert_name) do user.name = user.name.downcase end user.name => "sAmplE" user.convert_name user.name => "sample"
簡単にですが、どのような利用シーンがありそうか次の例題で見ていきましょう。
例題
先ほどのサンプルにコードを追加します。
# サンプルモデル class User include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Validations::Callbacks attribute :name, :string before_validation :convert_name # 追加 validates :name, format: { with: /\A[A-Z]+\z/, message: '英字かつ大文字のみが使用できます' } # 追加 def convert_name self.name = name.upcase end end
そして name
の format に関するバリデーション(全て大文字かどうか)がきちんと動くかをテストでチェックしたいとします。
問題は convert_name
が before_validation
のタイミングで呼ばれるため、下のようなテストを書いてしまうと name
は大文字となり常にバリデーションをパスしてしまいます。この例だとそりゃそうなのですが、コールバックに依存せずバリデーションそのものをチェックしておきたい・・・!みたいなシーンが訪れるかもしれません。
# テストが上手くいかない例 require 'rails_helper' RSpec.describe User do let(:user) { User.new(name: name) } describe 'バリデーションの確認' do subject { user.valid? } describe 'nameのバリデーションの確認' do context '全て小文字のとき' do let(:name) { 'sample' } it 'falseになること' do # `valid?` のときに `convert_name` がコールされてしまうため name は 必ず upcase になってしまう。 is_expected.to eq(false) # テスト失敗・・・ end end end end end
このようなときに、ブロック渡しを使えばチェックしたい内容をうま〜く実行することができます。
# テストが上手くいく例 require 'rails_helper' RSpec.describe User do let(:user) { User.new(name: name) } describe 'バリデーションの確認' do subject { user.valid? } describe 'nameのバリデーションの確認' do context '全て小文字のとき' do let(:name) { 'sample' } # 今回の例題ではブロックを渡さず allow(user).to receive(:convert_name) のみでもオッケーですが、 # 何らかの理由でできない場合とご想像ください。 before do allow(user).to receive(:convert_name) do user.name = name end end it 'falseになること' do is_expected.to eq(false) # user.errors.messages => {:name=>["英字かつ大文字のみが使用できます"]} end end end end end
このブロック渡しのポイントとしては、モックしたメソッドがコールされたタイミングでブロックの内容が実行されるという点です。
そのため、例えば、
- インスタンス生成時点で値をセットすると困る
- コールバックが複数用意されていてコールバックの実行順番は維持したまま実行内容だけ変更したい
といった場合に書き方の選択肢の一つとなり得ると考えます。
ただし、メソッドを丸っとモックすることになるため、用法用量には十分ご注意ください。
おわりに
かれこれ長いこと RSpec にはお世話になっていますが、最近初めて知った内容でしたのでご紹介しました。
テストを書くうえで選択肢の一つとなれば嬉しいです。
マネーフォワード福岡開発拠点では、エンジニアを募集しています!
福岡開発拠点のサイトもあるのでぜひみてください!
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【会社情報】 ■Wantedly ■株式会社マネーフォワード ■福岡開発拠点 ■関西開発拠点(大阪/京都)
【SNS】 ■マネーフォワード公式note ■Twitter - 【公式】マネーフォワード ■Twitter - Money Forward Developers ■connpass - マネーフォワード ■YouTube - Money Forward Developers