SQLite3 メモリ DB での順序依存テスト失敗を防ぐ修正
Rails の Active Record テストスイートを SQLite3 のインメモリデータベースで実行すると、テスト実行順に依存した数千件の失敗が発生していました。本 PR は、SchemaCache の破壊とデータベース破棄という二つの根本原因を除去し、テストの安定性を回復します。
背景
テストの順序依存は、ARCONN=sqlite3_mem bundle exec rake sqlite3_mem:test を走らせた際に顕在化し、特定のテストが実行された後に以降のテストが「テーブルが見つからない」エラーや属性欠損で失敗する状態でした。原因は主に二点に集約されます。
-
DirtyTest#test_field_named_fieldがActiveRecord::Base.clear_cache!を呼び出し、グローバル SchemaCache を空のインスタンスに置き換えていたため、続くテストはテーブル情報を取得できず例外を投げました。 -
AdapterConnectionTestの一部sql_notificationsテストがrun_without_connectionを使用した際、SQLite の:memory:データベースではremove_connectionがメモリ DB を破壊し、再接続後にスキーマが失われていました。
この二つの問題が組み合わさり、テスト全体が不安定になるという事象が発生していました。
技術的な変更
SchemaCache の汚染を防ぐ修正(dirty_test.rb)
test_field_named_field はテーブルを一時的に作成・削除し、ensure 句で clear_cache! を呼んでいました。この実装を次のように置き換え、SchemaCache への直接的なクリア操作を排除しました。
- class Testings < ActiveRecord::Base; end
- def test_field_named_field
- ActiveRecord::Base.lease_connection.create_table :testings do |t|
- t.string :field
- end
- ensure
- ActiveRecord::Base.lease_connection.drop_table :testings rescue nil
- ActiveRecord::Base.clear_cache!
- end
+ def test_field_named_field
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "pirates"
+ attribute :field, :string
+ end
+ assert_nothing_raised { klass.new.attributes }
+ end
変更点は
* 一時テーブル testings の生成・削除を廃止し、既存の pirates テーブルを再利用。
* attribute API を使用した匿名クラスで属性を定義。
* clear_cache! 呼び出しを除去し、グローバルキャッシュの破壊を防止。
メモリ DB での接続破壊を回避(adapter_test.rb)
AdapterConnectionTest の sql_notifications 系テストを unless in_memory_db? ガードで囲み、インメモリ環境では実行しないようにしました。また、テスト内部で通知キャプチャを行うロジックを明示的に記述しています。
- test "suppresses notifications when sql_notifications=false" do
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge(sql_notifications: false))
- notifications = capture_notifications("sql.active_record") { Post.first }
- assert_empty notifications
- end
- end
-
- test "sql_notifications are enabled by default" do
- notifications = capture_notifications("sql.active_record") { Post.first }
- assert_not_empty notifications
- end
+ unless in_memory_db?
+ test "suppresses notifications when sql_notifications=false" do
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.merge(sql_notifications: false))
+ notifications = capture_notifications("sql.active_record") { Post.first }
+ assert_empty notifications
+ end
+ end
+
+ test "sql_notifications are enabled by default" do
+ notifications = capture_notifications("sql.active_record") { Post.first }
+ assert_not_empty notifications
+ end
+ end
主なポイントは
* in_memory_db? ヘルパーでインメモリかどうか判定。
* ガード内にテスト全体を移動し、run_without_connection がメモリ DB を破壊するケースを除外。
* 期待する通知の有無を明示的にアサート。
設計判断
この PR では テストコードレベルでの防御 を選択し、コアライブラリの振る舞いを変更せずに問題を解消しました。具体的には
* グローバル状態(SchemaCache)への直接的なクリア をやめ、代わりに既存テーブルと属性 API を活用することでテストの自己完結性を確保。
* インメモリ DB の特性 を判定する in_memory_db? ガードを導入し、破壊的な接続操作を回避。これにより、他のテストや本番コードへの影響を最小限に抑えています。
* 変更は テストファイルのみ に留まり、アプリケーションロジックや設定に副作用を与えない設計です。結果として、既存テストの実行順序に依存しない安定したテスト環境が提供されます。
まとめ
本修正は、SQLite3 のインメモリデータベース使用時に発生していた SchemaCache の汚染 と 接続破壊 という二つの根本原因をテストコード側で切り離すことで、数千件に及ぶ順序依存テスト失敗を根絶しました。テストの信頼性が向上したことで、ローカルや CI 環境でのフィードバックが一貫し、今後の機能追加やバグ修正の検証コストが低減します。