Ruby refute と assert_not の違いを調査

Rails のテストと Minitest を学習していて、refuteassert_not は同じものなのか違うものなのか気になったので調べてみた。

アサーション

アサーション(assertion)とは、オブジェクトや式を評価して、期待された結果が得られるかどうかをチェックするコードのこと。

Rails 標準のテストのアサーション抜粋

アサーション 目的
assert A A が true であることを検証する
assert_equal A(期待値), B(実際の値) A == B であることを検証する
assert_not A A が false であることを検証する
assert_not_equal A B A != B であることを検証する
assert_empty A A が空(empty?)であることを検証する

2.6 利用可能なアサーション Rails テスティングガイド - Railsガイド

ここで疑問が。チェリー本(プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで Software Design plus)で Minitest を勉強したとき、assertの反対はrefuteだったはず。

assert の反対は refute では?

Rails コーディングルールはこうなっているようだ。

refuteではなくassert_notを使用すること。
Ruby on Rails に貢献する方法 - Railsガイド

実際にテストを書いてみると、Rubocop に refute_equal ではなく assert_not_equal の方がいいよ!と指摘された。

f:id:masuyama13:20200807105841p:plain

refuteassert_not にどういう違いがあるのか気になって Minitest のドキュメントを見たら、やはりfalseを確認するのはrefuteとなっていて、assert_notがつくものを見つけることができなかった。

Module: Minitest::Assertions — Documentation for minitest (5.14.1)

上のテストは Minitest で書いていたので、 assert_not_equal に書き換えると以下のようなエラーが出た。

Error:
FizzbuzzTest#test_15を渡すと文字列Buzzを返さない:
NoMethodError: undefined method `assert_not_equal' for #<FizzbuzzTest:0x00007fd79e2286e8>
Did you mean?  assert_equal
    fizzbuzz_minitest.rb:26:in `test_15を渡すと文字列Buzzを返さない'

まとめると、

  • Rails コーディングルール:refuteではなくassert_notを使う
  • Rubocop:refuteではなくassert_notを使う
  • Minitest:refuteを使う。assert_notはない

以上のことから、assert_not系は Minitest にはないもので、Rails のテストにはある、ということがわかった。Rails のテストってなんだっけ?

Rails 標準のテストクラスは、ActiveSupport::TestCaseを継承している。ActiveSupport::TestCaseは、Ruby 標準のテストであるMinitest::Testを継承している。

つまり、ActiveSupport::TestCaseの独自拡張部分か!

ActiveSupport::Testing::Assertions

コードを見比べる

ということで、やっとたどり着いた。"active_support/testing/assertions" で定義されていた。

assert_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 のコード(Minitest)

  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

コードから、refuteassert_notも同じと考えてよさそうだ。

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

https://github.com/rails/rails/blob/77932446895bd70f38a3aaa1ab288bf8a7b7142c/activesupport/lib/active_support/test_case.rb

余談

Rails じゃないけど、なんとかして assert_not を使ってみたい。特に意味はないけど…。試行錯誤の結果、Rails のテストと同じようにActiveSupport::TestCaseクラスを継承したテストクラスを作ったら、できた!

require "minitest/autorun"
require "active_support/all"
require "./fizzbuzz"

class FizzbuzzTest < ActiveSupport::TestCase
  def setup
    @fb = Fizzbuzz.new
  end

  def test_15を渡すと文字列Buzzを返さない
    assert_not_equal "Buzz", @fb.fizzbuzz(15)
  end
end

https://github.com/masuyama13/fizzbuzz/blob/master/fizzbuzz_active_support_test.rb

わーい

ところで、試行錯誤中いろいろツイートしてたら、Rubocop コミッターの方にリプライいただきちょっと感動。