😺

【フロントエンドが初めて触る Rails 】controllerのテストでこけたらmodelのafter_saveを疑う!

2024/10/04に公開

概要

Railsでrequestのテストを書いていた時、controlle内では1度しか読んでないはずのメソッドがなんで2回このメソッド呼び出されているんだ!!
ということが起こり、どうしてもテストが通らないという現象に陥りました。

調べていくと、どうやらmodel内のafter_save(コールバック)なるものにcontrollerで記述した同じメソッドが記述されていたため、2度メソッドが呼び出されていたことが判明しました。

そもそもafter_saveの存在が頭になかったことが原因のため、何が起きていたのか理解しつつ記事にしてみようと思いました。

after_saveとは

after_saveは、Railsのコールバックの一種で他にもafter_commitafter_createなどがあります。
modelで定義しておいたメソッドをafter_saveに入れておくと、createupdateの後が起きたときに自動的にそのメソッドを実行してくれます。
(コールバックを調べるとトランザクション関連がよく出てきますが、長くなりそうなのでここではスルーします!)

after_saveを使った具体例

例えば、ユーザーが作成(create)または更新(update)された後に、メールを送信する処理をafter_saveで行いたい場合、以下のようにmodelを書きます。

class User < ApplicationRecord
  after_save :send_notification

  private

  def send_notification
    if saved_change_to_created_at?
      # 新規ユーザー作成時
      UserMailer.welcome_email(self).deliver_later
    elsif saved_changes?
      # ユーザー情報更新時
      UserMailer.update_email(self).deliver_later
    end
  end
end

このコードでは、after_saveが呼び出されるたびに、ユーザーが新しく作成されたか、既存のユーザーが更新されたかを判別し、それに応じてメールを送信します。

  • saved_change_to_created_at? は、オブジェクトが新しく作成されたかどうかを判別するメソッド
  • saved_changes? は、オブジェクトが更新されたかどうかを確認

これにより、createでもupdateでも、ユーザーのデータが保存された後に適切なメールを送信できます。

テストで起きた問題

直面した問題では以下のようなcontrollerを実装し、あるメソッドがcontroller内で1度、model内のafter_saveで1度の合計2回呼ばれ、テスト内では1度の呼び出しの想定だったこともあり失敗し続けてしまいました。
例えば、次のようなコードです。

class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    if @user.save
      # この時点でafter_saveが発動してメールが送られる
      send_additional_email(@user)
    end
  end

  private

  def send_additional_email(user)
    # 任意のメソッドを使ってメール送信
    UserMailer.additional_info_email(user).deliver_later
  end
end

この例では、createアクションでユーザーが作成されると、まずafter_saveが実行され、send_notificationメソッドが呼ばれます。
その後、controller内でもsend_additional_emailが呼ばれ、同じユーザーに対して2つのメールが送信されることになります。

この例ではメールという複数回受信していることが明らかに目に見える形なので、動作確認で気づくことができますが、そうでない場面も多く実際の問題では全く気づくことができませんでした。

まとめ

after_saveというコールバックがメソッドを自動で呼び出していたために、テストが思ったように動かなかったという問題があり、かなり沼にハマってしまいました。
controllerでの処理でupdatecreateなどmodelに関連するメソッドを使用する際には、modelのafter_saveなどのコールバックで何をしているか注意することが大切そうです。
controller側で同じ処理を再度実行する必要がないか確認し、もし必要ない場合はモデルのコールバックに任せる形に整理するのが良さそうに思いました。

Discussion