ActiveRecord::Base.with_failsave (2)
実際に ActiveRecord::Base.with_failsave でテストを書いているうちに、いくつか使い勝手の悪い部分が見つかった。というわけで、すこし改良。
class ActiveRecord::Base
def create_or_update_with_fail; false end
alias_method :create_or_update_without_fail, :create_or_update
# ブロックが与えられた場合はブロックを実行し、そのあいだは save! や save が必ず失敗する
#
# ActiveRecord::Base.with_failsave do
# ...
# end
#
# また、サブクラスのみに適用することもできる。
#
# SomethingModel.with_failsave do
# ...
# end
def self.with_failsave
# ActiveRecord::Base のサブクラスで呼び出された場合は alias_method によって、
# サブクラス側にも create_or_update が定義されてしまうためか、再度の alias_method による
# 定義の差し替えだけでは元の挙動に戻らない。そのため、ensure 節で remove_method している。
subclass = !private_instance_methods(false).include?("create_or_update")
alias_method :create_or_update, :create_or_update_with_fail
yield
ensure
alias_method :create_or_update, :create_or_update_without_fail
remove_method :create_or_update if subclass
end
end
変更点はふたつ。
- ブロック実行中に例外が発生しても、create_or_update が元に戻るように
- ActiveRecord::Base のサブクラスだけに適用することもできるように
後者は functional test で全然意図しないモデルの保存に失敗して先に進めなかった経験から実装した。たとえば、User モデルの save だけを失敗させたい場合は ActiveRecord::Base.with_failsave としているところを、こんなふうに変えればいい。
User.with_failsave do
...
end
alias_method の挙動がよく分かってないので、コメントに書いていることとか間違ってるかも。