🤯️
rspec-rails にて power_assert を assert の名前で使えるようにする手順がかなりしんどかった
はじめに
rspec-rails にて power_assert を assert { 1 + 2 == 3 }
のような形で使いたい。ただそれだけのことが一筋縄ではいかなかったので誰の役にも立たない記録を残しておく。
実際の Rails アプリの spec/rails_helper.rb
にコピペするとしたときにわかりやすいよう必要な部分だけ if でインデントしてある。
前提知識1: 素の RSpec に minitest は入っていない
require "bundler/inline"
gemfile do
gem "rspec"
end
require "rspec/autorun"
describe do
it do
expect(1 + 2).to eq(3)
respond_to?(:assert) # => false
end
end
- 何の設定もしなければ使いやすいとは言えない expect が有効になる
- もちろん minitest のアサーションは入っていない
前提知識2: rspec-rails は minitest を強制的に導入する
require "bundler/inline"
gemfile do
gem "rails"
gem "rspec-rails"
end
require "rails"
require "action_view"
require "action_controller"
require "rspec/rails"
require "rspec/autorun"
RSpec.configure do |config|
config.include RSpec::Rails::RailsExampleGroup
end
describe do
it do
expect(1 + 2).to eq(3)
assert_equal 3, 1 + 2
assert 1 + 2 == 3
end
end
- minitest が必要なら本来
expect_with :minitest
と書く決まりになっている - しかし rspec-rails はそれとは無関係に minitest を適用している
- 理由はマッチャの一部を minitest の assertion に委譲しているため
- つまり rspec-rails は minitest をベースにしているとも言える
- そういうことなら expect で人々を苦しめなくても、かわりに assert 系メソッドを使う選択肢もあったと最初に広言してくれてもよかったと思う
rspec-power_assert 編
require "bundler/inline"
gemfile do
gem "rspec-power_assert"
gem "rails"
gem "rspec-rails"
end
require "rails"
require "action_view"
require "action_controller"
require "rspec/rails"
require "rspec/autorun"
RSpec.configure do |config|
config.include RSpec::Rails::RailsExampleGroup
end
if true
require "rspec-power_assert"
RSpec::Rails::Assertions.remove_method(:assert)
RSpec::PowerAssert.example_assertion_alias :assert
end
describe do
it do
assert { 1 + 2 == 3 }
end
end
- rspec-power_assert は minitest に依存しない形で power_assert に対応した is_asserted_by メソッドを生やしてくれる
- そのような場合は、変にこだわりを持たず、用意してくれたものをそのまま使う適応力や柔軟性が大切だとはいえ、シンプルに assert の延長として使いたい自分としてはどうしてもこの長いメソッド名を受け入れがたかった
- たんに
assert { }
として使いたかった - rspec-power_assert はそういうときのためにもエイリアスできるようにしてくれている
- だが普通にやると minitest の assert に負ける
- そこでまず RSpec::Rails::Assertions から assert をぶっ殺す
- 続けて
RSpec::PowerAssert.example_assertion_alias :assert
すると minitest に勝てる - ここで重要なのが明示的に
require "rspec-power_assert"
した直後でやること-
require "rspec-power_assert"
の替わりに .rspec のなかで-r rspec-power_assert
としてしまうと実行順序の関係で minitest に負ける
-
- こうしてなんとか使えるようになった assert だが minitest の assert との互換性がない
- 具体的には assert をブロックなしで呼べない
- 今後、rspec-rails に assert をブロックなしで呼ばれたり、今もどこかでそんな使われ方をされているとエラーになるかもしれない
- 心配であれば is_asserted_by を assert にエイリアスするのはやめたほうがよさそう
test-unit (+ power_assert) 編
require "bundler/inline"
gemfile do
gem "test-unit"
gem "rails"
gem "rspec-rails"
end
require "rails"
require "action_view"
require "action_controller"
require "rspec/rails"
require "rspec/autorun"
RSpec.configure do |config|
config.include RSpec::Rails::RailsExampleGroup
end
if true
# test-unit のメソッドを上書きしやがるメソッドを空で上書きする
# /opt/rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-rails-6.0.3/lib/rspec/rails/adapters.rb
RSpec::Rails::MinitestAssertionAdapter::ClassMethods.module_eval do
def define_assertion_delegators
end
end
RSpec.configure do |config|
config.expect_with :test_unit
end
end
describe do
it do
assert { 1 + 2 == 3 }
end
end
- test-unit は power_assert を内部で利用しているため test-unit を導入するだけで
assert { }
が使えるようになる - そして再度書くと rspec-rails は minitest の使用を前提としている、というか勝手に適用している
- 自分はそこまでの内情を知らなかったし、RSpec の設定で
expect_with :test_unit
と書けてしまうので test-unit が普通に使えるようになるものだと考えていた - しかし、実際は適用した test-unit を minitest が上書きしてしまう (メソッドがほぼ同じなため上書きされたことにも気づきにくい)
- これは it の中で
self.class.ancestors.collect(&:name).grep(/unit|mini/i)
としてみるとわかりやすい -
Test::Unit::Assertions
よりRSpec::Rails::MinitestAssertionAdapter
の方が直近にでてくるためTest::Unit::Assertions
が完全に負けているのがわかる - しょうがないので test-unit を上書きしやがる
MinitestAssertionAdapter
の該当メソッドを空で上書きしてやる - これでやっと
assert { }
が使えるようになる - 上書きしやがるメソッドを上書きする方法以外では
RSpec::Rails::Assertions.remove_method(:assert)
としてもいいが assert 以外のメソッドは minitest に上書きされたままになってしまっているので根っこから上書きされないようにした方がよい - 何度も書くが rspec-rails は minitest が動いている前提なので中身が test-unit に置き換わって今後も安全に動作するかは怪しい
- なので心配なら rspec-rails で test-unit を使うのはやめたほうがいい
minitest-power_assert 編
require "bundler/inline"
gemfile do
gem "minitest"
gem "minitest-power_assert"
gem "rails"
gem "rspec-rails"
end
require "rails"
require "action_view"
require "action_controller"
require "rspec/rails"
require "rspec/autorun"
RSpec.configure do |config|
config.include RSpec::Rails::RailsExampleGroup
end
if true
require "minitest-power_assert"
Minitest::Assertions.prepend Minitest::PowerAssert::Assertions
end
describe do
it do
assert { 1 + 2 == 3 }
assert 1 + 2 == 3
end
end
- minitest-power_assert は assert をブロック付きにして power_assert 対応してくれる
- しかし
require "minitest-power_assert"
としても、そのままでは使えない- minitest-power_assert は Minitest::Test にある assert メソッドを拡張している
- しかし rspec-rails は Minitest::Assertions を見ている
- つまり minitest-power_assert は拡張しているところが空振っている
- おそらく prepend がない、または使いづらい時代だったせいで Minitest::Assertions に直接適用するのが難しかったのか、そもそも Minitest::Assertions モジュールだけがひっぱり出されて RSpec で使われることなんか想定外だったのではないかと思われる
- そこで試しに Minitest::Assertions に prepend してみると
assert { }
が使えるようになった - assert はブロックなしでも呼べる (重要)
- 元の仕様と互換性があるので minitest-power_assert を入れたことで rspec-rails と minitest の関係に不整合が生じる恐れはないと思われる
まとめ
- どの方法もすんなりとはいかない
- Rails で RSpec を使った時点でレールから外れているのにさらに外れると非常に険しい
- rspec-power_assert の is_asserted_by を assert に alias するのは危ない
- rspec-rails と test-unit の相性は最悪
- minitest-power_assert で既存の assert を拡張する方法がわりかし安全と思われる
Discussion