【Rails】Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper 警告の解決パターン

## 発生する問題

既存のRailsアプリケーションをRails 6やRails 6.1にアップグレードすると、テストを実行したりした際に次のような警告が出る場合がある。

```
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

In order to autoload safely at boot time, please wrap your code in a reloader
callback this way:

    Rails.application.reloader.to_prepare do
      # Autoload classes and modules needed at boot time here.
    end

That block runs when the application boots, and every time there is a reload.
For historical reasons, it may run twice, so it has to be idempotent.

Check the "Autoloading and Reloading Constants" guide to learn more about how
Rails autoloads and reloads.
 (called from <main> at /Users/jnito/dev/sg/secure-sketch/config/environment.rb:5)
```

この記事ではこの警告の対処方法について、いくつかの解決パターンを説明する

## 自作クラスに問題がある場合

エラーメッセージに表示されているクラスが`ActionText::ContentHelper and ActionText::TagHelper`ではなく、自作クラスになっている場合は`config/initializers`ディレクトリに書いた初期化処理を修正すると直ることが多い。

```
# 自作のYourCustomKlassに問題がある、というようなメッセージが表示されている場合を想定する
DEPRECATION WARNING: Initialization autoloaded the constants YourCustomKlass.
```

たとえば以下のような初期化処理を書いていた場合は、

```ruby:config/initializers/your_custom_klass.rb
YourCustomKlass.do_configure
```

次のように`Rails.application.reloader.to_prepare`のブロック内で初期化処理を書くと警告が出なくなる可能性が高い。（最初に載せた警告文にもその対処法が書いてある）


```ruby:config/initializers/your_custom_klass.rb
Rails.application.reloader.to_prepare do
  YourCustomKlass.do_configure
end
```

そもそもRailsとしてお行儀の悪いコード（Railsの規約に則っていないコード）を書いていると、問題の解決がさらに難しくなる可能性がある。
「規約？Zeitwerk？何それ？」という人は以下のページをよく読んで、Rails wayに則ったコードを書く努力をしよう。

- [定数の自動読み込みと再読み込み \(Zeitwerk\) \- Railsガイド](https://railsguides.jp/autoloading_and_reloading_constants.html)

## それ以外の場合にまず試すこと

エラーメッセージに表示されているクラスが自作クラスでない場合（Railsのクラスや外部gemのクラスである場合）は、読み込んでいるgemに原因があると思われる。
最新のgemを使うと解決することもあるので、Rails本体を含めて可能な限りすべてのgemを最新版にし、警告の有無を確認する。

すべてのgemを最新化する場合の手順は以下の記事が参考になる。

- [永久保存版！？伊藤さん式・Railsアプリのアップグレード手順 \- Qiita](https://qiita.com/jnchito/items/0ee47108972a0e302caf)

それでも直らない場合はRailsの以下のissueを参照する。

- [Misleading deprecation warning when referencing ActionController::Base · Issue \#36546 · rails/rails](https://github.com/rails/rails/issues/36546)

上記のissueはかなり長いスレッドになっているが、よく読むと「私はこうすれば直った」「自分の場合はこのgemが原因になっていた」という書き込みが多数あるため、自分のRailsプロジェクトにも適用できる知見がどこかにあるかもしれない。

## 最後の手段：素のRailsから順を追って犯人捜しをする

とはいえ、この警告はそれでも直らないことがよくある。特に、エラーメッセージの表示が`ActionText::ContentHelper and ActionText::TagHelper`となっている場合は根本原因がつかみにくく、思いもよらないコードが警告の引き金になっていることがある。

筆者は先日、前述の解決策をすべて試したが警告がなくならなかったため、次のような方針で警告の原因を突き止めた。
簡単に説明すると次のような手順になる。

- できるだけ素のRailsに近い状態でテストを実行する（この状態だとおそらく警告は出ない）
- そこからgemを少しずつ戻していき、警告の有無を確認する。警告が出たらその直前に戻したgemに原因がある

詳細な手順を以下に述べる。
ただし、非常に地道で面倒な作業なので、最低でも1時間はかかることを覚悟した方がよい。

### 検証用の軽いテストを1つ選ぶ

検証用のテストを1つ決める。このあとに何度もテストを実行して警告の有無を確認するため、できるだけ軽いテストが良い。
たとえばバリデーションの結果を確認するだけのモデルのテスト（モデルスペック）などがそれに該当する。

このテストを実行して警告が表示されることを確認する。

```
$ bundle exec rspec spec/models/user_spec.rb
```

次の作業へ進む前に：このあとの作業ではコードをどんどん変えていくので、gitを使っていつでもコードを元の状態に戻せるようにしておくこと。

### RailsとRSpecをいったん素の状態にする

ここからRailsとRSpecをなるべく素の状態（デフォルトに近い状態）にする。

まず、`spec/rails_helper.rb`と`spec/spec_helper.rb`を作り直す。


```
$ git rm spec/rails_helper.rb
$ git rm spec/spec_helper.rb

# rspecのインストールコマンドで上で削除したファイルを作り直す
$ rails g rspec:install
```

次にGemfileをRailsのデフォルト状態にする。
http://railsdiff.org/ などにアクセスすると自分が使っているRailsのバージョンのデフォルトのGemfileが確認できるので、現在のGemfileはすべてコメントアウトして、デフォルトのGemfileの記述をコピペする。

ただし、RSpecを使っている場合はrspec-railsが必要になるので、このgemだけを追加しておく。
また、データベースとしてPostgreSQLやMySQLを使っている場合は、sqlite3 gemをpg gemやmysql2 gemに置き換える。

例としてRails 6.1.2.1のデフォルトのGemfile + rspec-railsを追加し、sqlite3 gemをpg gemに置き換えたコードを示す。（3行目の`ruby '2.5.0'`もコメントアウトした）

```ruby:Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# ruby '2.5.0'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem 'rails', '~> 6.1.2', '>= 6.1.2.1'
# Use sqlite3 as the database for Active Record
# gem 'sqlite3', '~> 1.4'
gem 'pg'
# Use Puma as the app server
gem 'puma', '~> 5.0'
# Use SCSS for stylesheets
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 5.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use Active Storage variant
# gem 'image_processing', '~> 1.2'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.4', require: false
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
end
group :development do
  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
  gem 'web-console', '>= 4.1.0'
  # Display performance information such as SQL time and flame graphs for each request in your browser.
  # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md
  gem 'rack-mini-profiler', '~> 2.0'
  gem 'listen', '~> 3.3'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
end
group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 3.26'
  gem 'selenium-webdriver'
  # Easy installation and use of web drivers to run system tests with browsers
  gem 'webdrivers'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

# 元のGemfileの内容はここから下ですべてコメントアウトしておく
# ...
# ...
# ...
# ...
```

この状態で`bundle install`を実行する。

### テストがパスするようになるまでコードを修正（主にコメントアウト）する

この状態でテストを実行してみる。

```
$ bundle exec rspec spec/models/user_spec.rb
```

が、おそらくエラーが出て起動しないはずだ。多くの場合、`config/initializers`の中でgemで導入されるはずのクラスが見つからない、というようなエラーが出ると思うので、いったんそうしたロジックはすべてコメントアウトする。

```ruby:config/initializers/paper_trail.rb
# たとえばPaperTrailクラスが見つからないなら、いったんコードはコメントアウト！
# PaperTrail.config.track_associations = true
```

「テストを実行→initializersのファイルをコメントアウト」を繰り返すと、次はinitializers以外の部分でエラーが出始める。
たとえば、`config/routes.rb`で`mount_roboto`メソッドが見つからない、というエラーが出た場合はroboto gemに依存しているコードなので、それもコメントアウトする。

```ruby:config/routes.rb
Rails.application.routes.draw do
  # roboto gemに依存したコードもコメントアウト！
  # mount_roboto
  root 'home#index'

  # ...
```

ただし、モデルの中で発生したエラーはモデルのテストを実行する上で必要なgemだったりするので、こちらはコードを直すのではなく、gemを復活させる。
たとえば、`app/models/user.rb`で`devise`メソッドが見つからない、というエラーが出た場合はGemfileでコメントアウトしていた`gem 'devise'`の行を元に戻して`bundle install`する。

```ruby:app/models/user.rb
class User < ApplicationRecord
  # モデルDeviseが使われているなら、コメントアウトではなくgemを復活させる
  devise :invitable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable
end
```

このような作業を繰り返し、Railsのデフォルトのgem + 必要最小限のサードパーティーgemで再びテストがパスするようにする。

### 警告が出るまでinitializersを元に戻してく

ここでは「Railsのデフォルトのgem + 必要最小限のサードパーティーgem」でテストがパスする状態になり、その状態であれば警告は出なかった、という前提で話を進める。

素のRailsに近い状態では警告が出ないということは、「素に近い状態」と「もともとのプロジェクトの状態」の間のどこかに警告の原因が隠れているということになる。

そこで1つずつコメントアウトしたinitializersのコードを元に戻し、テストを実行して警告の有無を確認していく。
もちろんinitializersのコードを戻したタイミングで、関連するgemも復活させる必要がある。

```ruby:config/initializers/paper_trail.rb
# さっきコメントアウトしたコードを元に戻す
# （このタイミングでGemfile中のpaper_trail gemも復活させる）
PaperTrail.config.track_associations = true
```

戻していくinitializersの数が多すぎる場合は、二分探索法を使ってまとまった単位で原因を探っていくのも良いと思う。

筆者のプロジェクトではこの過程で、[email_prefixer](https://github.com/wireframe/email_prefixer/pulls)というgemが警告の発生原因になっていることを突き止めた。

### 元のプロジェクトの状態でそのgemが唯一の原因か否かを確認する

警告の発生原因となるgemが見つかったら、`git stash`を使ってそこまでの変更を一時保存する。
そして元のプロジェクトの状態に戻して、エラーの発生原因となったgemを除外して警告の有無を確認する。

たとえば、email_prefixer gemを除外してテストを実行すると警告が出なくなったのであれば、email_prefixer が唯一の原因であることが確定する。そうでなければ、email_prefixer 以外にも警告の発生原因となっているgemが存在していることになる。

筆者のプロジェクトでは email_prefixer を除外しても警告が出続けていたので、 email_prefixer 以外のgemも原因になっていることがわかった。

### ふたたび原因探しの旅に戻る（ちょっとずつ元の状態に戻していく）

`git unstash`で原因調査中の状態にロールバックし、原因探しを再開する。

initializersをすべて元に戻しても警告が出ない場合は、initializersに出てこないgemが警告の原因になっている可能性が高い。
そこで、Gemfileでコメントアウトしていた行を元に戻していきながら、警告が発生するポイントを見つける。

大きなプロジェクトでは大量のgemを読み込んでいると思うので、二分探索法などを使ってある程度まとまった単位で復活させる方が良い。

筆者はdevelopmentグループを全て戻す → testグループを全て戻す → development, testグループをすべて戻す、というような順番で原因調査をしていった。

この過程で [email-spec](https://github.com/email-spec/email-spec) というgemを導入すると警告が発生することを突き止めた。

### 犯人を完全に特定する

先ほどと同じく`git stash`で元のプロジェクト状態に戻し、email_prefixerとemail-specを除外してテストを実行してみた。すると警告が出なくなった。
email_prefixerとemail-specのどちらかを復活させると警告が発生した。

このことから、警告の原因はこの2つのgemであることが確定した。（犯人特定！）

### 問題を修正する・その1＝プルリクエストを投げる

残念ながら原因を特定しただけでは警告をなくすことはできない。
もしそのgemが不要なgemであれば、「今後は使わない」で終わる話だが、多くのプロジェクトではそういうわけにはいかないだろう。

というわけで、今度はそのgemのどのコードが原因になっているのかを突き止める必要がある。
たとえば、email_prefixerでは以下のコードの下から5行目と4行目が原因となっていた。

```ruby
# https://github.com/wireframe/email_prefixer/blob/v1.2.0/lib/email_prefixer/railtie.rb#L10-L11
module EmailPrefixer
  class Railtie < ::Rails::Railtie
    initializer 'email_prefixer.configure_defaults' do |app|
      config = EmailPrefixer.configure do |config|
        config.application_name ||= app.class.parent_name
        config.stage_name ||= Rails.env
        config.builder ||= EmailPrefixer::Configuration::DEFAULT_BUILDER
      end
      interceptor = EmailPrefixer::Interceptor.new(config)

      # この2行が原因！！
      ActionMailer::Base.register_preview_interceptor(interceptor)
      ActionMailer::Base.register_interceptor(interceptor)
    end
  end
end
```

Rails 6.0以降ではRails関連のクラスは次のように遅延初期化の仕組みを使って、目的の処理を実行する必要がある。

```diff
-ActionMailer::Base.register_preview_interceptor(interceptor)
-ActionMailer::Base.register_interceptor(interceptor)
+ActiveSupport.on_load :action_mailer do
+  register_preview_interceptor(interceptor)
+  register_interceptor(interceptor)
+end
```

さらに、gemの多くはOSSなので、コードを修正したらプルリクエストを投げることが望ましい。
筆者もこの変更点を適当するためのプルリクエストを投げた。

- [Load action\_mailer lazily by JunichiIto · Pull Request \#6 · wireframe/email\_prefixer](https://github.com/wireframe/email_prefixer/pull/6)

プルリクエストがマージされるまでは、プロジェクト内では自分のリポジトリのコードを参照するようにする。

```ruby:Gemfile
# https://github.com/wireframe/email_prefixer/pull/6 がマージされリリースされるのを待つ
gem 'email_prefixer', github: 'JunichiIto/email_prefixer', branch: 'rails-6-lazy-init'
```

### 問題を解決する・その2＝Gemfileでrequire: falseを指定する

もう1件のemail-specは古いgemでrailtieの仕組みを使っていなかった。
そもそもこのgemはEメールのテストに便利なヘルパーメソッドを集めただけのgemなので、`spec/rails_helper.rb`の中でrequireすれば十分だった。

そこで、Gemfile内では`require: false`を指定して、アプリの初期化時にはrequireされないようにした。

```ruby:Gemfile
group :test do
  # ...
  gem 'email_spec', require: false
  # ...
end
```

email_specは`spec/rails_helper.rb`の中でrequireする。


```ruby:spec/rails_helper.rb
# ...

require 'email_spec'

# ...

RSpec.configure do |config|
  # ...
  config.include EmailSpec::Helpers
  config.include EmailSpec::Matchers
  # ...
end
```

これで警告が出ないようにすることができた。

### 問題を解決する・その3＝他のgemにリプレースする、自前で実装する
筆者のプロジェクトでは実施しなかったが、このほかにも同じような機能を持った他のgemにリプレースする、もしくはgemと同等の機能を自前で実装して警告を無くす、といった方法も考えられる。

## まとめ

というわけで、この記事ではRails 6.0以降で発生する`Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.`の警告に対処する方法を説明した。

上のemail_prefixerの修正例を見てもわかるように、gem内で参照していたクラスはActionMailer::Baseなのに、警告で表示されるクラスはActionText::ContentHelper と ActionText::TagHelper であるため、警告メッセージの内容がほとんど原因究明の役に立たないのがこの警告の辛いところだ。

この記事が同じような問題で困っている方の参考になれば幸いである。