Railsのテストを自動操縦:開発者が書かないコードを書くエージェントの構築
Railsチームが新機能のテストを後回しにする問題を解決するため、ソースコードを読み取りRSpecテストを自動生成・改善する自律型エージェントを開発した。このエージェントはCI/CDパイプライン内で人間の介入なく動作し、テストカバレッジとスタイル規則を検証する。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
大規模な Rails モノリシックアプリケーションの多くにおいて、組織は新機能の実装を優先し、それに対するテスト作成には後回しにしがちです。時間が経つにつれて、テストされていないコードが徐々に増え、チームは痛みを伴うバグのデバッグにより多くの時間を費やすことになります。
私たちはそのギャップを埋める自律型エージェントを構築しました。このエージェントは Rails のソースファイルを読み込み、RSpec テストを生成または改善し、スタイルルールとカバレッジ目標に対して検証を行います。さらに、人間の介入なしで CI/CD パイプライン内で実行されます。このような大規模なコードベースを扱うために、並列処理で動作します:複数のインスタンスが異なるファイルを同時に処理します。
エージェントの視点から見た RSpec
Ruby は動的型付け言語であり、コンパイルステップが存在しないため、エラーは実行時に表面化します。私たちのエージェントにとって、これはテスト構文を検証する唯一の方法が実際に実行することであることを意味します。RSpec は標準的な Rails テストフレームワークですが、テストを表現豊かで読みやすくしますが、そのドメイン固有言語は誤りやすいものです。
エージェントが Ruby on Rails のコードベースを読み込む際、5 つの主要なファイルタイプ(モデル、シリアライザー、コントローラー、メールラー、ヘルパー)を読み込みます。それぞれ構造が異なり(したがってテスト方法も異なります)、エージェントは各タイプに対して固有の指示を必要とします。
一つの利点は、ソースファイルから spec ファイルへのマッピングがほぼ 1:1 であることです。一般的な規約は以下の通りです:

ただし、このルールにはいくつか例外があります。例えば、app/controllers/ディレクトリがspec/requests/にマッピングされる場合や、単一のソースファイルに対して複数の spec ファイルが存在する場合などです。その場合の慣習は以下の通りです。

この単純なマッピングにより、任意のファイルに対応するテストを容易に見つけたり、全くテストが存在しないファイルを特定したりすることが可能になります。
しかし、当社のエージェントにとって難しくなるのは、コードの重複を防ぐために RSpec が共有コンテキスト(shared context)に大きく依存している点です。具体的にはファクトリ(factories)、フィクスチャ(fixtures)、データベーススキーマなどが該当します。
- ファクトリ:事前に定義された属性を持つテストオブジェクトを作成するための再利用可能なテンプレートであり、一貫性のあるテストデータの生成を容易にします。
- フィクスチャ:テストデータベースレコードをプリロードする静的データファイルであり、テストのための固定された基準値を提供します。
ファクトリファイルが存在しない場合はエージェントが作成し、存在する場合はそれを再利用します。ファクトリは spec ファイルとは異なり多くのテストで共有されるため、不用意な変更は他の場所のテストを簡単に壊す可能性があります。そのため、これらのファイルへの更新には注意が必要です。
Vibe を用いたエージェントの構築
当社は、Mistral のオープンソースコーディングアシスタントである Vibe 上に本エージェントを構築しました。デフォルトのシステムプロンプトはこのプロジェクトで十分であったため、私たちは以下の3つのレバーに焦点を当てました:リポジトリレベルのコンテキスト、専門的なスキル、およびカスタムツールです。
コンテキストエンジニアリング
コンテキストエンジニアリングは、私たちのアプローチの中核でした。Vibe はリポジトリレベルの AGENTS.md ファイルをサポートしており、このファイルがルートのリポジトリで実行される場合、その内容は自動的にシステムプロンプトに追加されます。
私たちが使用した AGENTS.md は、対象となるリポジトリに関する基本的な詳細を提供しましたが、主にエージェントに対して段階的な実行計画を提供するものでした:
各ステップには、何を行うべきか、そして成功の基準は何かという詳細が含まれていました。また、エージェントに方向性を示すことが重要だと感じた領域については、RSpec のベストプラクティスもいくつか記載しました。例を挙げると:
私たちは、エージェントが時折メソッドをスキップしたり、エッジケースのテストを怠ったりすることがあることに気づきました。つまり、完全に見えた仕様のコードを生成しながらも、ソースファイルから一部の公開メソッドを静かに無視してしまうのです。これに対処するため、AGENTS.md の最後には強制的な自己レビューを追加しました。エージェントは完了する前に、ソースファイルを再読し、「すべての公開メソッドをテストしたか?数を数えよ」と自らに明示的に問いかける必要があります。何か不足していれば、やり直します。
このように、厳格な計画に従うようエージェントを強制する汎用的な AGENTS.md ファイルを用いた結果、フレームワークレベルの指示を含む単一のマークダウンファイルだけで、品質スコアは0.68 から 0.74 に向上しました。
SKILLS ファイルの使用:
AGENTS.md のステップ 4 を思い出してください:
- ソースファイルの場所に基づき、正確に 1 つのスキルを選択して読む
単一の汎用的なスキルでは中途半端な結果しか得られません。モデルファイルをテストするための指示は、コントローラーファイルをテストするための指示とは異なるからです。
成功したのは、各カテゴリごとに別のスキルファイルを作成し、通常の Ruby ファイル用にも 1 つ用意したことです。
コントローラーのテスト用の基本的なスキルファイルの例を以下に示します:
カスタムツールの追加
Vibe には、ファイルの読み書きや bash コマンドの実行、コード編集を行うための組み込みツールが備わっています。しかし、そのアクション空間を広げるためにカスタムツールや MCP(Model Context Protocol)にも対応しています。このプロジェクトでは、カスタムツールが鍵となりました。
いくつかの実験を経て、以下の構成を採用しました:
- RuboCop リンティングツール:rubocop コマンドを実行してスタイル違反を検出し、エージェントがコードに戻って修正を行うようにします。
- SimpleCov ツール:RSpec と統合され、コードカバレッジとテストの正しさを同時にチェックします。これを最終ステップとして配置したことが重要でした。これにより、エージェントが作成するすべてのテストが実際に実行されることを保証できます。
以下は、RuboCop リンティングツールの実装を簡略化したバージョンです:
SimpleCov ツールも同様のパターンに従いますが、より多くの機能を持っています。RSpec を使って spec ファイルを実行し、各テストのパス/フェイル結果を収集して、ソースファイルのカバレッジパーセンテージを報告します。エージェントは、どのテストが失敗したか(エラーメッセージ付き)と、ソースファイルのどの行が一度も実行されなかったかを取得できます。これにより、必要に応じて自己修正するための十分な情報を得ることができます。
このツールを最初に接続した際、生成されたテストのうち初回実行で成功したのは約 3 割だけでした。しかし、エージェントは数回の反復で全ての失敗を自己修正しました。このステップがなければ、一部のテストは実行されなかったでしょう。
テスト品質の測定
良いユニットテストは、Arrange-Act-Assert パターンに従います:状態を設定し、1 つの動作を実行し、結果を検証します。正確なアサーションを使用し、ハッピーパスとエラーパスの両方をカバーし、何が壊れたかを明確に特定できる失敗を生成すべきです。

ツールベースの指標
既存の Ruby on Rails のツールを使用すれば、いくつかの指標を容易に取得できます:
- RSpec: Ruby 向けの最も人気のあるテストフレームワーク。読みやすく構造化されたテストを書くための表現力豊かな構文を提供します。
- RuboCop: スタイルガイドの遵守を支援しコードの一貫性を保つための、静的な Ruby コード解析器およびフォーマッターです。
- SimpleCov: テストスイートによって実行されるコードの量を測定する、Ruby 向けのコードカバレッジツールです。
これらのツールから、以下のいくつかの有用な定量的指標を抽出できます:テストのパス率、ファイルあたりの RuboCop ビオレーション数、ユニットテストスイートでカバーされているコードの割合、および実行速度です。これらの指標は、テストスイートの全体的なパフォーマンスを測定するための堅牢なベースラインを提供します。
しかし、これらの数字だけでは、テストファイルが実際に良いかどうかを判断することはできません。両方とも RuboCop ビオレーションがゼロで、カバレッジが 100%、かつすべてのテストにパスしている 2 つのファイルをどのように比較すべきでしょうか?定量的な指標は同じように見えるかもしれませんが、テスト品質における質的な違いが存在する可能性があります。
LLM-as-a-judge の背後にあるアイデアは、LLM に「すべてのエラー条件がテストされているか?」や「これは良いテストか?」と問いかけ、明確な基準に基づいて 0 から 1 のスコアを返させることです。
例となるプロンプト:
ここでの「スコアリングルール」セクションは特に重要です。これにより、実行間でのスコアの整合性が保たれます。これがなければ、エージェントはスコアリングの厳格さや緩やかさをどのように定義すべきかという明確な基準を欠いてしまいます。
このようなスコアは、ツールの出力や徹底的な開発者によるレビューに比べると権威性は劣ります。しかし、リポジトリ内のすべてのファイルでスケールして実行される場合など、現在のバージョンのエージェントがどの程度機能しているかを示す良い指標となります。
LLM-as-a-judge 評価の限界:
テキスト化できるものであれば何でも LLM-as-a-judge でスコアリング可能であるため、これは汎用性が高いと言えます。ただし、出力されるスコアは決定論的ではありません。*temperature=0* を設定していても、カーネル操作の順序によるばらつきが生じます。実際には、同じ評価に対する出力は常に近い値になりますが、この非決定性は特定のユースケースでは課題となり得ます。
私たちのユースケースでは、一度に単一のテストではなく通常数十から数百のテストをスコアリングするため、これは許容できる限界でした。集約されたスコアは一貫して保たれていました。
欠落した括弧の問題
LLM-as-a-judge にはもう一つの核心的な問題がありました。テスト自体は高品質であっても実行されない場合や、背後にあるロジックが根本的に間違っている場合があります。
例:
このテストは素晴らしいです。1 つの振る舞いを検証し、ドキュメントとして読める明確な記述を持ち、Arrange-Act-Assert パターンに従っており、もし壊れた場合でも有用なメッセージで失敗します。
このテストを、以前に定義したスコアリングプロンプトを用いて複数のモデルで実行しました。得られた平均スコアは 0.75 です。サッと目を通す開発者なら、おそらく同意するでしょう。
しかし、このテストには 1 つの根本的な欠陥があります:これは実行できません。User.new() の閉じ括弧 ) が抜けているためです。これはこのテストを明らかに不適切にする致命的な欠陥です。
この例は緩和しやすいですが、構文エラーを引き起こす可能性は他にもたくさんあります:
- バージョンエラー:互換性のない Ruby や Rails のバージョンからメソッドや構文を使用している
- 存在しないメソッド:一部のメソッドが削除された、名前が変更された、あるいは最初から存在しなかった
- ファクトリ/フィクスチャエラー:欠落したファクトリの参照、誤った属性、または欠落した関連付けの参照
- 依存関係の欠如:テスト環境に含まれていない gem やモジュールの使用
実験
エージェントが実際のコードベースに耐えられるかを確認するため、実験を行いました。対象のリポジトリには 275 のソースファイルが含まれていました。その半分はすでにテストカバレッジを持っており、残りの半分は何も持っていませんでした。
エージェントを、既にテストがあるかないかに関わらず、すべてのファイルに向けて実行しました。未カバーのファイルについては、ゼロから仕様に生成することに成功しました。すでにテストが存在するファイルについては、それらを再書き起こしして改善しました。
LLM-as-a-judge を用いて、各 spec ファイルをテスト前とテスト後に評価しました。その結果、テスト済みファイルの集計スコアは 0.49 から 0.74 に向上し、カバレッジは 100% に達しました。まだ改善の余地はありますが、このアプローチの有効性が実証されました。
MetricResult
Files processed: 275
Tests passing: 100%
Average line coverage (SimpleCov): 100%
RuboCop violations after self-correction: 0
LLM-as-a-judge score: 0.74
By file type
すべてのファイルタイプがテストしやすいわけではありませんでした。Models は最高スコアを獲得しました。そのビジネスロジックは完結しており、テストパターンも予測可能だからです。一方、Controllers は難易度が高く、HTTP リクエスト処理の追加によりエラーが発生する余地が大きくなりました。
File type | LLM-as-a-judge
---|---
Models | 0.81
Controllers | 0.67
Serializers | 0.80
Does it run?
RSpec の実行最終ステップとして SimpleCov をバンドルし、エージェントが作成したすべてのテストを実行させるよう強制したことが、最も影響力のある決断でした。最初の試行で合格したのはテストのわずか 3 分の 1 に過ぎませんでした。しかし、エージェントは数回の反復内ですべての失敗を自己修正しました。
このステップがなければ、LLM-as-a-judge は実行されていないこれらのテストを高品質と評価していただろうという点です。これは、大規模化における「括弧の欠落」問題に他なりません。
Vibe is open source: ここで説明したエージェント、ツール、スキルはすべて Vibe 上で動作します。ぜひお試しください。
原文を表示
In most large Rails monoliths, organizations prioritize writing new features over writing tests for them. Over time, more and more code goes untested, forcing teams to spend more time debugging painful bugs.
We built an autonomous agent that closes that gap. It reads Rails source files, generates or improves RSpec tests, validates them against style rules and coverage targets, and runs inside a CI/CD pipeline with no human intervention. To operate on codebases at this scale, it runs in parallel: multiple instances working on different files simultaneously.
RSpec through an agent's eyes
Ruby is dynamically typed: there is no compilation step, so errors surface at runtime. For our agent, this means the only way to verify test syntax is to execute it. RSpec, the standard Rails testing framework, makes tests expressive and readable, but its domain-specific language is easy to get wrong.
When the agent reads a Ruby on Rails codebase, it reads five main file types (models, serializers, controllers, mailers, helpers), each structured differently (therefore tested in different ways). The agent needs distinct instructions for each type.
One benefit: the mapping from source file to spec file is nearly 1:1. The general convention is:

There are a few exceptions to that rule however, like that app/controllers/ are sometimes mapped to spec/requests/, or that sometimes a single source file can have multiple spec files, in which case the convention is:

This straightforward mapping makes it easy to locate the tests for any given file, or to identify files that lack tests entirely.
Where it gets harder for our agent is that to avoid duplicating code, RSpec relies heavily on shared context: factories, fixtures, database schemas...
- Factories: Reusable templates for creating test objects with predefined attributes, making it easy to generate consistent test data.
- Fixtures: Static data files that preload test database records, providing a fixed baseline for tests.
If a factory file doesn’t exist, the agent creates it; if it does, the agent reuses it. Because factories are shared across many tests (unlike spec files), careless changes can easily break tests elsewhere, so updates to these files must be made with caution.
Building the agent with Vibe
We built the agent on top of Vibe, Mistral's open-source coding assistant. The default system prompt was sufficient for this project, so we focused on three levers: repository-level context, specialized skills, and custom tools.
Context engineering
Context engineering was central to our approach. Vibe supports a repository-level AGENTS.md file: when running on a repository with this file at its root, its contents are automatically appended to the system prompt.
The AGENTS.md we used provided basic details about the target repositories, but mostly, it provided the agent with a step-by-step execution plan:
Each step included details about what to do and what the success criteria are. We also included some best practices of RSpec on areas where we felt it was important to orient the agent. Example:
We found the agent would sometimes skip methods or leave edge cases untested: it would generate a spec that *looked* complete but quietly ignored a few public methods from the source file. To counter this, the AGENTS.md ends with a forced self-review: the agent must re-read the source file and explicitly ask itself "Did I test every public method? Count them." before finishing. If anything is missing, it goes back.
With this generic AGENTS.md file forcing the agent to follow strict planning, our quality score went from 0.68 to 0.74, all from a single markdown file with framework-level instructions.
Using SKILLS files:
Recall step 4 of our AGENTS.md:
4. Choose and read exactly one skill based on the source file location
A single generic skill would produce mediocre results: the instructions precise enough for testing a model file are the wrong instructions for a controller file.
What worked was creating a separate skills file for each category, plus one for plain Ruby files.
Here is an example of a basic skills file for testing controllers:
Adding custom tools
Vibe has built-in tools for reading and writing files, running bash commands, and editing code. But it also supports custom tools & MCPs to expand its action space. For this project, custom tools were key.
After some experimentation, we went for:
- A RuboCop linting tool that runs the rubocop command to detect style violations, so that the agent goes back to the code and fixes them.
- A SimpleCov tool integrated with RSpec that checks both code coverage and test correctness. Placing this as the final step was key: it guarantees every test the agent writes actually runs.
Below is a stripped-down version of how we implemented the Rubocop linting tool:
The SimpleCov tool follows the same pattern but does more: it runs the spec file with RSpec, collects pass/fail results for each test, and reports the coverage percentage of the source file. The agent gets back which tests failed (with error messages), and which lines of the source file were never exercised. This gives it enough to self-correct when needed.
When we first wired this tool in, only around a third of generated tests passed on first execution. The agent self-corrected all failures within a few iterations. Without this step, some of those tests would not have ran.
Measuring test quality
A good unit test follows the Arrange-Act-Assert pattern: set up state, execute one behavior, verify the outcome. It should use precise assertions, cover both happy and error paths, and produce failures that clearly identify what broke.

Tool-based metrics
Existing Ruby on Rails tools allow us to get several metrics easily:
- RSpec: the most popular testing framework for Ruby. Expressive syntax for writing readable, well-structured tests.
- RuboCop: a static Ruby code analyzer and formatter, helping enforce style guidelines and keeping code consistent
- SimpleCov: a code coverage tool for Ruby, measuring how much code is executed by the test suite.
From these tools, we can extract several useful quantitative metrics: the proportion of tests that pass, the number of RuboCop violations per file, the proportion of code covered by the unit test suite, and the speed of execution. These metrics provide a solid baseline for measuring the overall performance of a test suite.
But these numbers alone cannot determine whether a test file is actually good. How should we compare two files that both have zero RuboCop violations, 100% coverage, and all tests passing? The quantitative metrics may look identical, yet qualitative differences in test quality can still exist.
LLM-as-a-judge
The idea behind LLM-as-a-judge is to ask an LLM: “Are all error conditions tested?” or “Is this a good test?” and get back a score from 0 to 1 based on clear criteria.
Example prompt:
The "Scoring Rules" section here is especially important as it enforces the score to be more consistent across runs. Without it, the agent would lack clear definition of how strict or how lax the scoring is.
Such a score is less authoritative than a tool's output or a thorough developer review. But it is a good indication of how well the current version of our agent is doing, especially when run at scale across every file in a repository.
Limitations of LLM-as-a-judge evaluation:
Anything that can be put into text can be scored with LLM-as-a-judge, which makes it versatile. But the score output is not deterministic. Even with *temperature=0*, there will be variance due to the order of kernel operations. In practice the outputs for the same evaluation are always close, but this lack of determinism can be an issue for some use cases.
For our use case, since we score not a single test but usually tens to hundreds at a time, this was an acceptable limitation. The aggregated score stayed consistent.
The missing parenthesis problem
There was another core problem with LLM-as-a-judge: a test can be of great quality but not run, or have the logic behind it be wrong altogether.
Example:
This test looks great. It tests one behavior, has a clear description that reads as documentation, follows Arrange-Act-Assert:, and would fail with a useful message if broken.
We ran this test through multiple models using the scoring prompt defined earlier. The average score given was 0.75. A developer taking a quick look would likely agree.
But this test has one core flaw: it will not run. All because of a missing closing parenthesis ) on the User.new(). This is a critical flaw that makes this test definitely not suitable.
This example is easy to mitigate. But there are a lot other possibilities that can lead to syntax error:
- Versioning errors: Using methods or syntax from an incompatible Ruby or Rails version
- Non-existent methods: some methods might have been removed, renamed, or never existed.
- Factory/fixture errors: Referencing missing factories, incorrect attributes, or missing associations
- Missing dependencies: Using gems or modules not included in the test environment
The experiment
We ran an experiment to check the agent could hold up against a real codebase. The target repository contained 275 source files. Half of them already had test coverage. The other half had none.
We pointed the agent at every file, whether it already had tests or not. For uncovered files, it generated specs from scratch successfully. For files that already had tests, it rewrote and improved them.
We scored every spec file before and after with LLM-as-a-judge. The aggregate score for tested files went from 0.49 to 0.74, and coverage reached 100%. There's still room for improvement, but the results validated the approach.
MetricResult
Files processed275
Tests passing100%
Average line coverage (SimpleCov)100%
RuboCop violations after self-correction0
LLM-as-a-judge score0.74
By file type
Not all file types were easy to test. Models scored highest: their business logic is self-contained and the test patterns are predictable. Controllers were harder, HTTP request handling introduced more room for error.
File typeLLM-as-a-judge
Models0.81
Controllers0.67
Serializers0.80
Does it run?
Bundling SimpleCov with RSpec execution as the final step, forcing the agent to run every test it wrote, was the single most impactful decision. Only a third of tests passed on first execution. The agent self-corrected all failures within a few iterations.
Without this step, LLM-as-a-judge would have rated these non-running tests as high quality: the missing parenthesis problem at scale.
Vibe is open source. The agent, tools, and skills described here all run on top of it. Try it now.
関連記事
News to Guide
ニュースの次に確認する
発表内容を、現在の料金や仕様と照らし合わせられる関連ガイドです。
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み