【フロントエンドが初めて触る Rails 】controllerのテストでこけたらmodelのafter_saveを疑う!
概要
Railsでrequestのテストを書いていた時、controlle内では1度しか読んでないはずのメソッドがなんで2回このメソッド呼び出されているんだ!!
ということが起こり、どうしてもテストが通らないという現象に陥りました。
調べていくと、どうやらmodel内のafter_save
(コールバック)なるものにcontrollerで記述した同じメソッドが記述されていたため、2度メソッドが呼び出されていたことが判明しました。
そもそもafter_save
の存在が頭になかったことが原因のため、何が起きていたのか理解しつつ記事にしてみようと思いました。
after_saveとは
after_save
は、Railsのコールバックの一種で他にもafter_commit
やafter_create
などがあります。
modelで定義しておいたメソッドをafter_save
に入れておくと、create
とupdate
の後が起きたときに自動的にそのメソッドを実行してくれます。
(コールバックを調べるとトランザクション関連がよく出てきますが、長くなりそうなのでここではスルーします!)
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での処理でupdate
やcreate
などmodelに関連するメソッドを使用する際には、modelのafter_save
などのコールバックで何をしているか注意することが大切そうです。
controller側で同じ処理を再度実行する必要がないか確認し、もし必要ない場合はモデルのコールバックに任せる形に整理するのが良さそうに思いました。
Discussion