👾

ClaudeでFixtureをFactoryBotに移行した

に公開

概要

  • RailsのMinitestで使用していたFixtureをFactoryBotに移行した。
  • Claude Codeで移行計画と実装を行い、人がレビューした。
    • 人が修正内容を確認できるように、小さいスコープを修正するサイクルを繰り返した。

想定読者

  • Railsアプリ開発者
  • テストの改善を検討している方

背景

  • 古いテストはFixtureに依存しているものが多いかつFixtureのYAMLファイルの数が多いため、Fixture起因のテスト失敗時の原因特定と、修正および修正による副作用の有無の確認が大変だった。

FixtureとFactoryBotの比較

Fixture (Rails標準) FactoryBot (Gem)
データの形式 静的 (YAMLファイル) 動的 (Rubyコード)
作成タイミング テスト開始時に一括投入 テストケースごとに作成
実行速度 非常に速い Fixtureに比べると遅い
柔軟性 低い(固定データ) 非常に高い(引数で変更可能)
バリデーション 実行されない 実行される(ActiveRecordを通る)
関連の扱い ID指定などで結合(少し面倒) アソシエーション定義で自動生成

※Gemini 3調べ

そもそもFixtureの運用の何が良くなかったのか

  • プロダクトコード側でモデルに変更(カラム追加など)を加えたときに、テストで意図せぬバリデーションエラーが発生することがあった。
  • モデルに変更があったとき、FixtureのYAMLファイルに記述された関連データの全てを修正する必要がある場合があった。
    • モデル間の依存関係をインスタンスのレベルで管理する必要があった。
  • fixtures :allによる無駄なデータの読み込みがあった。
    • YAMLファイルは100個以上あり、テストごとに必要なYAMLファイルのみを読み込むように管理するのは大変そうだった。
      • などと書いたがこの程度ならfixtures :allした方が速いしYAMLファイルを分割するよりも楽。FactoryBotに移行するよりこのままの方が速い。

FactoryBotへの移行に何が期待できるのか

  • テストデータは動的に作成されるため、モデルに変更があったときにのFactoryの修正は少ない。
  • アソシエーションは自動で生成されるため、Factoryのコード量は比較的少なく保てる。
  • 個々のテストに必要なデータだけを作成する運用が簡単。
  • (主観)Fixtureはインスタンスのレベルでテストデータを管理するが、FactoryBotはモデルやアソシエーションのレベルでテストデータを管理する。そのため抽象化したビジネスロジックをより強く意識して実装することができ、Railsアプリ内のドメイン知識の学習効率が上がる。

移行戦略

  • 実行計画と実装はClaude Codeで行う。
    • ファイル数が多く作業量が果てしないのでAIを利用する。
    • 人がレビューしやすいように小さいスコープに区切って修正するサイクルを繰り返す。
  • レビューは人が行う。
    • 完全な自動移行はテスト品質の低下に気付けない可能性があると思ったので、レビューの工程は必須にした。
    • 現エンジニアがジョインする以前からのテストが多く存在するため、ドメイン知識の喪失を防止するために内容を確認することにした。
    • 移行完了後に必要なテストと不要なテストの仕分けをしたいので、おおまかにどのようなテストがあるか見たかった。

実際の変更作業

人のレビュー負荷を低く保つ(レビューするのはする)ことを重視してClaudeに実装計画の指示を出すと以下のような計画が立てられた。

全体方針

段階的に移行する。移行中も全テストが常にパスし続ける状態を維持するため、fixtures :allの除去は最後のステップまで温存する。

フェーズと順番の理由

Phase 1 — 共有ヘルパーの移行

多数のテストから呼ばれるログイン機能等のヘルパーメソッドを最初に移行する。共有コードを先に片付けないと後続フェーズで同じ修正が大量のファイルに散らばるため、他の全フェーズの前提条件とする。

Phase 2 — モデルテスト

他のレイヤーへの依存がなく独立性が高いモデルテストから着手する。ここでFactoryのtrait設計(ユーザーの状態、コンテンツの属性など)を固めることで、後続フェーズの移行がスムーズになる。参照数の少ないファイルから始めて段階的に複雑なものへ進める。

Phase 3 — コントローラーテスト

Phase 1のヘルパー整備・Phase 2のFactory設計が揃った状態で着手する。参照数の少ないファイルから始め、複雑なファイルを後回しにする。

Phase 4 — 決済・購入・広告テスト

複数レイヤー(コントローラー・サービス・ジョブ)にまたがり、かつ参照数も全体で最大級のファイルが集中するドメイン群であるため、汎用的なFactoryが揃うPhase 3以降に配置する。

Phase 5 — サービステスト

単体の独立性は高いが、一部のファイルはレコードの生成順序や関連の複雑さから全面的な再設計が必要なため、最後に対処する。

Phase 6 — クリーンアップ

全テストファイルからFixture参照が除去された後に、YAMLファイルを段階的に削除し、最後に fixtures :all を完全除去する。各削除グループの後に全テストを通して安全を確認しながら進める。

設計上のポイント

常にデプロイ可能な状態を維持する

各ステップ完了後に全テストがパスすることを確認してから次へ進める。YAMLを削除するのはFixture参照がゼロになってからとし、移行中のテスト破損を防ぐ。

参照数の少ないファイルから着手する

各フェーズ内で参照数の少ないファイルを先に処理する。Factoryの設計を段階的に洗練させながら進められ、複雑なファイルに取り掛かる頃には必要なFactoryが揃った状態になる。

Integration Testの干渉に注意する

ActionDispatch::IntegrationTestはトランザクションのロールバックだけではクリーンなテスト環境が保証されない場合があるが、Fixtureはテスト実行前にテーブルをtruncateしてからYAMLをロードするため、テスト間のDB状態は常にリセットされ、暗黙的にクリーンアップされていた。YAMLを削除したらtruncateが走らなくなり、テスト間のレコード蓄積による次のようなケースでのテスト失敗が顕在化した。

  • :delete_allを呼ぶテストの後続テストが参照
  • .first/.second等の実行順依存
  • 固定IDを持つマスターレコードのPK衝突
  • 固定ID挿入後のPostgreSQLシーケンスずれ

対応方針を「各テストが前のテストの状態に依存しないよう自己完結させる」こととし、以下の対応を行なった。

  • find_or_createパターンで冪等な生成にする
  • create(...)の戻り値を直接参照して順序依存を排除する
  • 固定ID挿入後はreset_pk_sequence!でシーケンスを正規化する

結果

移行後、テスト時間は長くなる

Fixtureは初めに全データを作成するので、データを準備する時間は短い。
単純にFactoryBotに移行するだけだとテスト実行時間は長くなる。
モデルのテストでDBにレコードを作成する必要がない場合はcreateではなくbuildを使った方が良い。

ただしFactoryBotへの移行により各テストが独立し可読性が向上するので、不要になったテストの削除などはしやすくなる(はず)。

良かったこと

  • ほとんどのテストデータがFactoryBotのtraitcreate/buildされるようになったので、テストケース内で作成されたインスタンスの性質を把握しやすくなった。
    • 今後テスト間の独立性を向上させる改善がしやすくなる(はず)。
  • インスタンスレベルのデータの管理からtraitレベルの管理になったことで単純に分量が減り、変更する際の認知負荷が下がった。(個人的にはこれが大きい)

おわりに

コーディングエージェントを利用する大きなメリットの一つは、FixtureからFactoryBotへの移行のような、一つ一つはそこまで複雑ではないが量が多いかつ静的解析ツールを使った自動化は難しい作業を任せられることだと思う。
やることが明確だが実行するのが大変な作業はAIが適しているが、具体的なイメージが想像できていないことをそのままAIに指示して出力させるのはかえって修正に工数を取られることもあるし学びもないので望ましくない。
対応の必要性を感じながらも自分でやる場合に想定される面倒さから躊躇してしまい負債を重ねてきたが、このような作業にこそAIを活用して楽しく開発を続けた〜い。

wwwave's Techblog

Discussion