ActiveRecord::Base.with_failsave (2)

2008/02/14 8:12am

実際に 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

変更点はふたつ。

  1. ブロック実行中に例外が発生しても、create_or_update が元に戻るように
  2. ActiveRecord::Base のサブクラスだけに適用することもできるように

後者は functional test で全然意図しないモデルの保存に失敗して先に進めなかった経験から実装した。たとえば、User モデルの save だけを失敗させたい場合は ActiveRecord::Base.with_failsave としているところを、こんなふうに変えればいい。

User.with_failsave do
  ...
end

alias_method の挙動がよく分かってないので、コメントに書いていることとか間違ってるかも。