環境
- Rails 8.0.x
- Devise 4.9.x
- RSpec + Capybara
ユーザーまたは管理者の認証機能にはdeviseを利用している環境です。
問題
System Specのbeforeブロックでsign_in userを呼ぶと、以下のエラーが発生する。
RuntimeError:
Could not find a valid mapping for #<User id: ...>
背景
specはこんな感じ
require 'rails_helper'
describe 'ユーザーログイン', type: :system do
let(:user) { create(:user) }
before do
sign_in user
end
it 'hogheohgoeghe' do
visit hoge_path
expect(page).to have_content 'hogehoge'
end
試したこと
| 条件 | 結果 |
|---|---|
| ファイル単体でspec実行 | 失敗 |
itブロック内でsign_inを呼ぶ |
成功 |
sign_in user, scope: :userと明示的にスコープ指定 |
成功 |
| 他のspecファイル2つ以上と一緒に実行 | 成功 |
原因
Rails8のルート遅延読み込み(Lazy Route Loading)によって、devise_forによるmappingが効かず、sign_inメソッドでエラーが発生する
Rails8のルート遅延読み込み(Lazy Route Loading)とは
開発、テスト環境において、Rails起動時にルーティングを遅延読み込みさせる設定のことです
これにより、数千単位でルーティングが設定してあるRailsアプリケーションの起動を早めることに成功しています。
This Pull Request changes engine and app route sets to a Rails::Engine::RouteSet, which knows about the current Rails application. The default middleware stack has also changed to include a Rails::Rack::LoadRoutes middleware that loads routes if needed. This PR loads routes under the following circumstances:
In dev/test:
- The first request via middleware
- When application or engine url_helpers.some_path is called via method_missing?
- When application or engine url_helpers.respond_to?(:some_path) is called via - respond_to_missing?
実際に、ルーティングが必要になるタイミングが来ないと読み込んでくれないみたいです
railsガイドには書いてないっぽいですね、記載があったらすみません
devise_forによるmappingとは
deviseを用いてルーティングを設定するときに、下記のように設定するとします。
devise_for :users
このdevise_forが実行されると、devise内部でDevise.mappingsにマッピング情報が登録されます。
このマッピングには以下のような情報が含まれます
- リソース名(
:user) - 使用するモデルクラス(
User) - 認証に関連するルート情報
- 使用するコントローラー情報
sign_inメソッドは、このマッピング情報を参照してユーザーを認証します。
# Devise内部のイメージ
Devise.mappings[:user] #=> #
解決策
1.rails_helper.rbでルートを強制ロード
spec/rails_helper.rbに以下を追加:
Rails.application.reload_routes_unless_loaded
全てのsystem specで、beforeブロック実行前にルートが確実にロードされる
2.scopeを明示的に指定
require 'rails_helper'
describe 'ユーザーログイン', type: :system do
let(:user) { create(:user) }
before do
sign_in user, scope: :user # ここに追加
end
it 'hogheohgoeghe' do
visit hoge_path
expect(page).to have_content 'hogehoge'
end
詳しくはdevise gemのdevise_forを読んで欲しいですが、明示的にスコープを指定してあげることもできます
3.eager_loadをtrueにする
config/environments/test.rbで
config.eager_load = true
起動時に全てがロードされるため問題は解決するが、テストの起動時間が長くなる
他にも方法はあるけど、ここまで
感想
- Railsガイドのドキュメントではなく、Ruby on Railsのリリースノート直接みた方が早かったかも
- PR最初から見ればよかった
参考
devise Devise::Mappingについて
Rails8遅延読み込みについてのPR