`eager_load=true` 起動時の NameError を修正し、再発防止の Rake タスクを追加
eager_load = true でテスト環境を起動すると発生していた NameError: uninitialized constant ViewComponent::SystemTestControllerNefariousPathError を修正し、同種の問題を検出する eager_load_check Rake タスクを追加しました。
背景
この NameError は、Zeitwerk の定数オートロードと Rails エンジンの初期化順序の組み合わせによって引き起こされていました。eager_load = true の環境では、Zeitwerk がファイルをオートロードする前に Rails エンジンのコントローラーが処理される必要がありますが、ViewComponent::Base が先にオートロードされると、view_components_system_test_controller.rb 内で参照される ViewComponent::SystemTestControllerNefariousPathError が未定義となっていました。
ViewComponent::SystemTestControllerNefariousPathError はエラークラス群をまとめた view_component/errors で定義されていますが、view_components_system_test_controller.rb はそのファイルを明示的に require していませんでした。開発環境ではオートロードが逐次行われるため問題が顕在化せず、eager_load = true を有効にした環境でのみ再現する典型的な「本番環境だけで起きるバグ」でした。
技術的な変更
修正は app/controllers/view_components_system_test_controller.rb への require 追加という最小限の変更です。
変更前:
# frozen_string_literal: true
class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
if Rails.env.test?
before_action :validate_file_path
変更後:
# frozen_string_literal: true
require "view_component/errors"
class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
if Rails.env.test?
before_action :validate_file_path
require "view_component/errors" を明示的に追加することで、コントローラーが読み込まれる時点でエラークラスが確実に定義済みとなり、ロード順序に依存しなくなります。
再発防止として、Rakefile に eager_load_check タスクが追加されました。このタスクは、コンポーネントを持たない最小限の Rails アプリを別プロセスで起動し、EagerLoadCheckApp.initialize! が正常完了するかを検証します。
desc "Verify the app boots with eager_load=true (catches missing requires)"
task :eager_load_check do
puts "Checking eager loading..."
result = system(
{"RAILS_ENV" => "test"},
"bundle", "exec", "ruby", "-e", <<~RUBY
require "rails"
require "action_controller/railtie"
require "view_component"
class EagerLoadCheckApp < Rails::Application
config.eager_load = true
config.secret_key_base = "test"
config.hosts.clear
end
EagerLoadCheckApp.initialize!
RUBY
)
abort("Eager loading check failed!") unless result
puts "Eager loading check passed"
end
コメントにも記されているとおり、意図的に「コンポーネントなし」の素の Rails アプリを使用しています。これにより、Zeitwerk が ViewComponent::Base を事前にオートロードしない状態、つまり実際のホストアプリケーションで問題が発生するシナリオを正確に再現します。このタスクは all_tests タスクにも組み込まれ、CI で継続的に実行されます。
設計判断
別プロセスで検証アプリを起動する方式が採用されています。
system() を使って Ruby インタープリタを別プロセスとして起動しているのは、現在のテストプロセスが既に view_component をロード済みであるためです。同一プロセス内でアプリを初期化しても、定数は既に定義済みとなるため問題を再現できません。別プロセスで「まっさらな状態」から起動することで、実際のホストアプリケーションが経験するロード順序を忠実に再現しています。また、config.hosts.clear によって ActionDispatch::HostAuthorization が検証をスキップするよう設定されており、検証に不要な設定を排除した最小構成になっています。
まとめ
require 1行の追加という最小限の修正でロード順序への依存を排除しつつ、再発を防ぐための CI チェックをセットで導入した変更です。eager_load_check タスクのアプローチ、すなわち「別プロセスで最小構成のアプリを起動して初期化を検証する」という手法は、Rails エンジンを提供するライブラリが eager_load 起因の問題を継続的に検出するための汎用的なパターンとして参考になります。