Rails assert_not が定義された経緯を調査

この記事の続き。

masuyama13.hatenablog.com

前回の疑問

assert_not_equalなどはエイリアスとして設定されてるっぽい。なんでassert_notエイリアスじゃだめなんだろう。

コードを見比べる

Minitest の refute

  def refute test, msg = nil
    msg ||= message { "Expected #{mu_pp(test)} to not be truthy" }
    assert !test, msg
  end

minitest/assertions.rb at master · seattlerb/minitest · GitHub

ActiveSupportassert_not

  def assert_not(object, message = nil)
    message ||= "Expected #{mu_pp(object)} to be nil or false"
    assert !object, message
  end

rails/assertions.rb at 77932446895bd70f38a3aaa1ab288bf8a7b7142c · rails/rails · GitHub

refuteと比べると、エラーメッセージの文言以外は同じようだ。

ActiveSupportassert_not_equalなど

refute_equalなどのエイリアス(別名)になっている。

module ActiveSupport
  class TestCase < ::Minitest::Test
    Assertion = Minitest::Assertion

    # 中略

    # test/unit backwards compatibility methods
    alias :assert_raise :assert_raises
    alias :assert_not_empty :refute_empty
    alias :assert_not_equal :refute_equal
    alias :assert_not_in_delta :refute_in_delta
    alias :assert_not_in_epsilon :refute_in_epsilon
    alias :assert_not_includes :refute_includes
    alias :assert_not_instance_of :refute_instance_of
    alias :assert_not_kind_of :refute_kind_of
    alias :assert_no_match :refute_match
    alias :assert_not_nil :refute_nil
    alias :assert_not_operator :refute_operator
    alias :assert_not_predicate :refute_predicate
    alias :assert_not_respond_to :refute_respond_to
    alias :assert_not_same :refute_same
    # 略
  end
end

rails/test_case.rb at 77932446895bd70f38a3aaa1ab288bf8a7b7142c · rails/rails · GitHub

alias new_method old_method で、old_methodnew_method という別名をつけられる。

alias :assert_not_equal :refute_equal というコードによって、assert_not_equalrefute_equal が実行される。

なぜ assert_notエイリアスじゃないのか?

refuteassert_not のコードを見比べたところ、違いはエラーメッセージだ。エラーメッセージを変えたかったから、エイリアスではなく新しいメソッドとして定義したのだろうか。

ここまでは前回の復習。

こういうことをつらつらと書いていたところ、フィヨルドブートキャンプのメンターでチェリー本の著者でもある伊藤淳一さんにヒントをいただいた。

この後「assert_not をわざわざ定義しているのは、エラーメッセージを変えたかったから?」という質問をしたところ、GitHub を見ればわかるかもしれないとアドバイスいただいた(本当はもっと詳しい説明とやり方まで動画で説明いただきました!ありがとうございます。フィヨルドブートキャンプ生なら見られます)。

あと RubyMine のコードジャンプ便利そう。ただ就職したときにどうなるのかが不安…。まずは VSCode でどのくらいできるか調べてみよう。

assert_not が定義された経緯を調査

コードをもう一度よく見たら、上に何やらコメントがたくさん。英語でしかもコメントだと、まるでそこに存在していないかのように無視してしまうことがよくある…。これ先に気づきたかった。

# Asserts that an expression is not truthy. Passes if <tt>object</tt> is
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
# like <tt>if foo</tt>.
#
#   assert_not nil    # => true
#   assert_not false  # => true
#   assert_not 'foo'  # => Expected "foo" to be nil or false
#
# An error message can be specified.
#
#   assert_not foo, 'foo should be false'
def assert_not(object, message = nil)
  message ||= "Expected #{mu_pp(object)} to be nil or false"
  assert !object, message
end

親切に例示まである。← これが RDoc というやつ?

英語は一生懸命勉強中だが、とりあえず上の3行を DeepL 翻訳にかけてみてから、自分でも考える。

Asserts that an expression is not truthy. Passes if object is nil or false. "Truthy" means "considered true in a conditional" like if foo.

表現が truthy ではないことを主張します。オブジェクトが nil か false の場合にパスするのです。"truthy" は if foo のように "条件付きで true とみなされる" ことを意味します。

という感じ?「エラーメッセージを変えたかったから、エイリアスではなく新しいメソッドとして定義した」という仮説は正しそうだ。 もう一つ、GitHub から履歴を見る方法を教えてもらった。

f:id:masuyama13:20200809223451p:plain

Blame をクリックすると、コードの変更履歴を見ることができる。assert_notの部分は8年前に変更されていることがわかった。

いくつかコメントがついていた。頑張って訳してみる。(Special Thanks: DeepL)

Introduce assert_not to replace 'assert !foo' · rails/rails@f75addd · GitHub

'assert !foo' の代わりに assert_not を導入


なぜ Minitest の refute を再実装するのですか?


美しさと後方互換性のために、従来の assert_* API を維持しています。これでニッチな部分を埋めることができます(このメソッドは異なるメッセージと戻り値を使うので、エイリアスはつけていません)。


説明ありがとうございます。後方互換とはどういうことですか? Rails master は >>1.9.3 ではないのですか?


superを呼び出すことで、メッセージをカスタマイズできます。

def refute obj, message = nil
  message ||= "Expected #{mu_pp(object)} to be nil or false"
  !super(obj, message)
end
alias :assert_not :refute

(戻り値を考慮して編集)


考え中です。それは#refuteを再利用していますが、その戻り値を変更しています。そしてassert_not用のRDocは、オリジナルのメソッドではなくエイリアスとして表示されます。

refuteを変更してassert_notをエイリアス化するよりも、assert_notがrefuteを呼び出すようにした方が良いと思います。しかし、そうするとコードは次のようになります。

  !refute(object, message)

vs

  assert !object, message

後者(オブジェクトが真でないと主張する)の方が、前者(オブジェクトが真であることを否定しない)よりも明確だと考えられます。


(訳などおかしかったら教えてください)

まとめ

(8月12日追記:相当勘違いしていたのでまた書き直します)

エラーメッセージの表現が適切ではないと考えられたため、エイリアスではなくassert_notというメソッドを別に定義したことがわかった。

refuteを上書きしてエイリアスにすることも検討されたが、戻り値を変更するのにドキュメント上はエイリアスとして表示されるのは不適切であることから、別のメソッドとして定義した方がよいと考えられて今の形になっている。

ということは、refuteを使ったときは Minitest のエラーメッセージが表示されるのか??という新たな疑問が。(続く)