はじめに、というか最初に結論
この記事では以下のようなRails(ActiveRecord)のコールバック仕様を説明します。
before_xxx が false を返した場合のみ保存処理が停止する
before_xxx が nil を返した場合や、after_xxx が false を返した場合はそのまま保存される
この仕様を知らなかった人は先に進みましょう。
知っていた人も興味があれば続きを読んでください。
動作確認したRailsバージョン
- Rails 4.2.4
 
Railsあるある問題のひとつ、「before_xxx の戻り値を間違える」
知っている人は知っている、そして知っている人でもたまにうっかり間違える、Railsのよくある落とし穴として before_valdiation や before_save の戻り値のワナがあります。
この問題については @suginoy さんがドンピシャな記事を書いてくれています。
rails3 - ActiveRecordのbefore_saveでうっかり - Qiita
以下は上の記事に載っているコード例です。
class Item < ActiveRecord::Base
  before_save :prepare_save # This callback doesn't validate
  def prepare_save
    if hoge_check_ok?
      self.checked = true
    else
      self.checked = false
    end
  end
end
このコードの問題点は次の通りです。
まず、before_save コールバックで false が返されるとそこで処理が止まってしまい、レコードが保存されないというのがRailsの(なかなかいやらしい)仕様です。
なので、else に入ってくるとコールバックの戻り値が false になり、処理が止まってしまいます。(そして大半のケースで、これはバグとして扱われます)
詳細は元の記事の説明を参照してください。
false と nil は同じか、別物か?
一方、Rubyでは false と nil を偽の値として扱い、それ以外の値を真として扱う、という言語仕様があります。
なので、コールバックの戻り値も false と nil はどちらも同じように扱われると思っていました。
が!!
before_xxx の場合は false だけが処理停止の条件になり、 nil は続行します。
これはこれでなんとも紛らわしい仕様だと思いませんか?
少なくとも僕はビックリしました。
公式ドキュメントにはどう書いてあるのか?
公式ドキュメントにはどう書いてあるのか調べてみました。
Canceling callbacks
If a before_* callback returns false, all the later callbacks and the associated action are cancelled.
以下ざっくり翻訳です。
コールバックのキャンセルについて
before_* コールバックが false を返した場合、それ以降の全コールバックと、呼び出し元のアクションはキャンセルされます。
うーん、「nil は違うよ!」と明示的に書いてもらわないと、Rubyの慣習的に「false も nil も一緒だよね?」と思えなくもありません。(どうでしょうか??)
テストを書いて確認してみた
というわけで、この仕様を確認するテストを書いてみました。
まず、こんなモデルを作ってみました。
コールバックメソッドを一通り定義しています。
(コールバックの一覧はこちらの公式ドキュメントに記載されています)
# user.rb
class User < ActiveRecord::Base
  validates :name, presence: true
  before_validation :method_before_validation
  def method_before_validation
    true
  end
  after_validation :method_after_validation
  def method_after_validation
    true
  end
  before_save :method_before_save
  def method_before_save
    true
  end
  before_create :method_before_create
  def method_before_create
    true
  end
  after_create :method_after_create
  def method_after_create
    true
  end
  after_save :method_after_save
  def method_after_save
    true
  end
  after_commit :method_after_commit
  def method_after_commit
    true
  end
end
それから、RSpecのテストコードはこんなふうに書きました。
rspec-parameterizedとモックを使って順番にコールバックの戻り値を true または false に切り換え、保存できる/できないを検証しています。
(RSpecのモックの使い方についてはこちらの記事をご覧ください)
# user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
  using RSpec::Parameterized::TableSyntax
  let(:user) { User.new(name: 'Alice') }
  example 'default implementation' do
    expect(user.save).to be_truthy
  end
  where(:callback, :value, :result) do
    :method_before_validation | false | false
    :method_before_validation | nil   | true
    :method_after_validation  | false | true
    :method_after_validation  | nil   | true
    :method_before_save       | false | false
    :method_before_save       | nil   | true
    :method_before_create     | false | false
    :method_before_create     | nil   | true
    :method_after_create      | false | true
    :method_after_create      | nil   | true
    :method_after_save        | false | true
    :method_after_save        | nil   | true
    :method_after_commit      | false | true
    :method_after_commit      | nil   | true
  end
  with_them do
    example 'callback value' do
      allow(user).to receive(callback).and_return(value)
      expect(user.save).to eq result
    end
  end
end
上のテストコードから result が false になるケース(つまり、保存できないケース)だけを抜き出すと以下のようになります。
:method_before_validation | false | false
:method_before_save       | false | false
:method_before_create     | false | false
これ以外はすべて result が true になります。
結論
上のテストからわかる結論は以下の通りです。
before_xxx が false を返した場合のみ保存処理が停止する
before_xxx が nil を返した場合や、after_xxx が false を返した場合はそのまま保存される
知らなかった人はこの機会に覚えておきましょう!
追記:Rails 5での仕様変更について
Rails 5では仕様が変わり、 before_xxx で false を返してもそのまま処理が続行されるようです。
コールバックで処理を停止したいときは throw :abort します。
before_save :ensure_admin
def ensure_admin
  throw(:abort) unless self.admin
end
こちらのスライドに詳しい情報が載っています。
Better Callbacks in Rails 5 // Speaker Deck
情報提供してくれた @suginoy さん、ありがとうございます!!