ActiveSupport::Testing::TimeHelpers を rspec-rails をロードする前に include すると travel_back が動かない

ActiveSupport::Testing::TimeHelperstravel_to を使って日時を固定するテストで、 それ以降に実行する他のテストでも日時が固定されたままになっている現象に遭遇した。

# foo_spec.rb
describe 'foo' do
  before { travel_to(Time.zone.now) }
  # ...
end

# bar_spec.rb
describe 'bar' do
  # 日時が固定されたまま
end

普通なら describecontext の単位で ActiveSupport::Testing::TimeHelpersafter_teardown が呼ばれて travel_back が実行されるので、テストごとに固定が解除されるはず。

# https://github.com/rails/rails/blob/v7.0.4/activesupport/lib/active_support/testing/time_helpers.rb#L70-L73
def after_teardown
  travel_back
  super
end

RSpec の設定を確認すると spec_helper.rbActiveSupport::Testing::TimeHelpersinclude していた。

# spec/spec_helper.rb
RSpec.configure do |config|
  require 'active_support/testing/time_helpers'
  config.include ActiveSupport::Testing::TimeHelpers

# ...

これを rails_helper.rb に移動すると、travel_back が実行されるようになった。

# spec/rails_helper.rb
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!

RSpec.configure do |config|
  config.include ActiveSupport::Testing::TimeHelpers # ここに移動

# ...

rspec-rails をロードすると after_teardown が呼ばれる仕組み

Rails アプリで RSpec のテストを書く場合、だいたいは rspec-rails gem を使うと思う。

上記の rails_helper.rb では rspec-rails をロード (require 'rspec/rails') した後に ActiveSupport::Testing::TimeHelpersinclude した。

require 'rspec/rails' すると Rails アプリのテストに便利な機能 (オフにしがちな use_transactional_fixtures とか) や独自のアサーションの他に、Rails 標準のテスティングフレームワークである Minitest のアサーションやライフサイクルをサポートするモジュールやクラスがロードされる

ロードされるモジュールのひとつである RSpec::Rails::MinitestLifecycleAdapter によって、Minitest::Test::LifecycleHooks と同等のフックがサポートされる。(これらのフックはライブラリから使うために用意されている)

# https://github.com/rspec/rspec-rails/blob/v5.1.2/lib/rspec/rails/adapters.rb#L76
group.around do |example|
  before_setup
  example.run
  after_teardown
end

これが rspec-rails をロードした後に ActiveSupport::Testing::TimeHelpersinclude すると after_teardown のフックが呼び出される仕組みになっている。

余談だが、独自にフックするメソッドを作成する場合は、処理の後に super を呼ばないと他のフックが呼び出されない。

def after_teardown
  # 最後に super を呼ぶ
  super
end

慣れていない人には分かりづらい spec_helper.rbrails_helper.rb

rails generate rspec:install を実行すると spec_helper.rbrails_helper.rb の 2 つが作成される。 それが慣れていない人にとってはどちらに書けば良いのか分かりづらいかもしれない。エラーになれば気付くことができるのだが、今回はとりあえず動くから気付くのが難しかった。 元々のコードは Active Support を require する時点で違和感を覚えるのだが、レビューが漏れていたのもある。

慣習として (?) spec_helper.rb には RSpec 独自の、rails_helper.rb には Rails 独自の設定を書くことになっているらしいが、皆さんどうしているのだろうか。 僕は初期状態のまま spec_helper.rbrails_helper.rb を分けている。

© 2023 暇人じゃない. All rights reserved.