Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

【RSpec】allow().to receive() にはブロックを渡せる

こんにちは、福岡開発拠点の小林です。 マネーフォワード クラウド経費マネーフォワード クラウド債務支払 の開発を担当しています。

この記事は 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_namebefore_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】 ■マネーフォワード公式noteTwitter - 【公式】マネーフォワードTwitter - Money Forward Developersconnpass - マネーフォワードYouTube - Money Forward Developers