[Rails] PostgreSQLアダプタのクラス変数初期化をテスト分離に対応
背景
RailsのPostgreSQLアダプタでは、型マッピングのカスタマイズを可能にする register_type_mapping メソッドが提供されています。このメソッドはクラス変数 @@type_mapping_callbacks にコールバックを蓄積し、後で型マップの初期化時に実行します。
以前のPR(おそらく型マッピング機能の改善)の実装では、クラス変数の初期化方法に問題があり、テストを分離実行(isolation mode)した際に失敗する事象が発生していました。この問題は、Railsのテスト実行戦略において重要な懸念事項でした。
技術的な変更内容
問題のあったコード
以前の実装では、||= 演算子を使用してクラス変数を初期化していました:
変更前:
def register_type_mapping(&block)
raise ArgumentError, "block required" unless block_given?
@@type_mapping_callbacks ||= []
@@type_mapping_callbacks << block
end
@@type_mapping_callbacks&.each { |block| block.call(m) }
修正後のコード
新しい実装では、defined? を使用して明示的にクラス変数の存在を確認するアプローチに変更されました:
変更後:
def register_type_mapping(&block)
raise ArgumentError, "block required" unless block_given?
@@type_mapping_callbacks = [] unless defined?(@@type_mapping_callbacks)
@@type_mapping_callbacks << block
end
@@type_mapping_callbacks = [] unless defined?(@@type_mapping_callbacks)
@@type_mapping_callbacks.each { |block| block.call(m) }
なぜこの変更が必要だったのか
Rubyのクラス変数は、クラス階層全体で共有される特性を持ちます。テスト分離実行時には、各テストケースが独立した環境で実行される必要がありますが、||= による初期化では以下の問題が発生します:
-
未定義状態の誤認識:
||=は変数がnilまたはfalseの場合に初期化を実行しますが、クラス変数が未定義の状態とnilが設定されている状態を区別できません -
Safe Navigation演算子の限界:
&.eachはnilの場合をスキップしますが、未定義の変数に対してはNameErrorを発生させる可能性があります
defined?(@@type_mapping_callbacks) を使用することで、クラス変数が実際に定義されているかどうかを確実に判定でき、テスト分離実行時の予期しない動作を防ぎます。
影響範囲
この変更は、PostgreSQLアダプタのカスタム型マッピング機能を使用しているアプリケーションに対して透過的です。動作に変更はなく、テストの信頼性が向上します。特に、以下のようなケースで恩恵があります:
- CI環境でのテスト並列実行
-
rails test --defer-outputを使用した分離テスト - Spring や Zeitwerk のリロード機能を使用する開発環境
まとめ
クラス変数の初期化パターンを ||= から defined? ベースに変更することで、テスト分離実行時の安定性が向上しました。この種の微妙な初期化問題は、大規模なテストスイートでのみ顕在化することが多く、早期の発見と修正が重要です。