0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIコーディングエージェントには「変更に関係するテストだけ」を実行させる — Jest / Vitest 標準機能でのテスト実行設計

0
Posted at

Claude Code などの AI コーディングエージェントにコード修正を任せると、修正のたびに npm test で全テストスイートを実行しようとする場面によく遭遇します。人間であればテスト出力を流し読みして失敗箇所だけを拾えますが、エージェントは標準出力をすべて読み、コンテキストウィンドウに蓄積した上で次の行動を決めます。1,000 件のテスト結果のうち本当に必要なのが「変更に関係する数件」だけだとすれば、残りはトークンの浪費であり、エージェントの判断材料を薄めるノイズです。

この課題はツール側でも公式に認識されています。Vitest 4.1 では agent レポーターが追加されました。失敗したテストとそのエラーだけを表示し、成功したテストの出力やログを抑制することでトークン消費を削減するモードで、AI コーディングエージェント内での実行を検知すると自動的に有効化されます(Vitest 4.1 リリースブログ)。

ただし、出力を削るだけでは「全テストを実行する時間」は変わりません。本記事では一歩進めて、実行するテスト自体を変更に関係するものだけに絞る方法を、小さなサンプルプロジェクトを作りながら確認していきます。Vitest を主軸に、Jest でも同じことが標準機能だけで実現でき、追加ツールは不要であることを示します。

この記事でわかること

  • Vitest の変更検知ベースのテスト実行を、手元で再現しながら確認する手順
  • 依存関係の推移的な追跡がどう働くか、どこで効かなくなるか
  • AI コーディングエージェント(Claude Code)・pre-commit hook・CircleCI への組み込み方
  • Jest で同じことを実現する場合の CLI オプションの対応

検証環境

対象 バージョン
Node.js(ローカル) v22.22.2
Node.js(CircleCI) cimg/node:22.14
Vitest 4.1.8

サンプルプロジェクトを用意する

依存関係の追跡を観察できる最小構成として、「cart.tsprice.ts に依存し、greet.ts は独立している」という 3 ファイル構成を作ります。

mkdir vitest-demo && cd vitest-demo
npm init -y
npm install -D vitest
mkdir src

ソースファイルを 3 つ作成します。

src/price.ts
export function taxIncluded(price: number): number {
  return Math.floor(price * 1.1);
}
src/cart.ts
import { taxIncluded } from './price';

export function cartTotal(prices: number[]): number {
  return prices.reduce((sum, p) => sum + taxIncluded(p), 0);
}
src/greet.ts
export function greet(name: string): string {
  return `Hello, ${name}!`;
}

それぞれに対応するテストを作成します。

src/price.test.ts
import { expect, test } from 'vitest';
import { taxIncluded } from './price';

test('taxIncluded adds 10% tax', () => {
  expect(taxIncluded(100)).toBe(110);
});
src/cart.test.ts
import { expect, test } from 'vitest';
import { cartTotal } from './cart';

test('cartTotal sums tax-included prices', () => {
  expect(cartTotal([100, 200])).toBe(330);
});
src/greet.test.ts
import { expect, test } from 'vitest';
import { greet } from './greet';

test('greet returns greeting', () => {
  expect(greet('CircleCI')).toBe('Hello, CircleCI!');
});

変更検知は git の情報を使うため、リポジトリとして初期化してコミットしておきます。

git init
git add -A && git commit -m "initial"

まず全件実行して、3 つのテストファイルが走るベースラインを確認します。

$ npx vitest --run

 RUN  v4.1.8 /home/user/vitest-demo

 ✓ src/price.test.ts (1 test) 4ms
 ✓ src/cart.test.ts (1 test) 3ms
 ✓ src/greet.test.ts (1 test) 3ms

 Test Files  3 passed (3)
      Tests  3 passed (3)

--changed で変更に関係するテストだけを実行する

src/price.ts に何らかの変更を加えてから(コミットはせずに)、--changed を付けて実行します。

$ npx vitest --changed --run

 RUN  v4.1.8 /home/user/vitest-demo

 ✓ src/price.test.ts (1 test) 3ms
 ✓ src/cart.test.ts (1 test) 3ms

 Test Files  2 passed (2)
      Tests  2 passed (2)

実行されたのは 2 ファイルだけです。注目すべきは cart.test.ts が含まれている点です。cart.test.tsprice.ts を直接インポートしていません。cart.test.tscart.tsprice.ts という静的インポートの連鎖を Vitest が辿り、推移的に影響を受けるテストとして選択しています。一方、依存関係のない greet.test.ts はスキップされました。

--changed は値なしの場合、未コミットの変更(ステージング済み・未ステージングの両方)を対象にします。--changed HEAD~1 で直前のコミット、--changed origin/main でブランチ間の差分を基準にすることもできます(Vitest CLI)。なお、差分が何もない状態で実行すると No test files found, exiting with code 0 と表示され、正常終了します。

vitest related: ファイルを明示して関連テストを実行する

git の差分ではなく、特定のソースファイルを起点に関連テストを実行することもできます。

$ npx vitest related src/price.ts --run

 ✓ src/price.test.ts (1 test) 3ms
 ✓ src/cart.test.ts (1 test) 3ms

 Test Files  2 passed (2)

結果は --changed と同じく推移的な依存を含む 2 ファイルです。--changedrelated は同じ依存グラフの仕組みを使うため、選択されるテストは一致します。こちらは後述する lint-staged との組み合わせで使います。Vitest はデフォルトでウォッチモードに入るため、エージェントや hook から呼ぶ場合は --run を付けて 1 回実行で終了させる点に注意してください。

agent レポーターで出力をさらに絞る

実行するテストを絞った上で、出力もエージェント向けに最適化できます。--reporter=agent を付けると、成功したテストの個別出力が抑制されます。全件成功時はサマリーのみになります。

$ npx vitest --changed --run --reporter=agent

 RUN  v4.1.8 /Users/hidetaka/sandboxes/vitest-run-changed-test

 Test Files  2 passed (2)
      Tests  2 passed (2)

テストが失敗した場合は、失敗したテストだけが詳細付きで出力されます。次は taxIncluded の税率を 10% から 8% に変更し、price.test.ts(110 を期待)と cart.test.ts(330 を期待)の期待値と食い違わせたときの出力です。

 ❯ src/price.test.ts (1 test | 1 failed) 3ms
   × taxIncluded adds 10% tax 3ms
 ❯ src/cart.test.ts (1 test | 1 failed) 3ms
   × cartTotal sums tax-included prices 3ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/price.test.ts > taxIncluded adds 10% tax
AssertionError: expected 108 to be 110 // Object.is equality

- Expected
+ Received

- 110
+ 108

 FAIL  src/cart.test.ts > cartTotal sums tax-included prices
AssertionError: expected 324 to be 330 // Object.is equality

- Expected
+ Received

- 330
+ 324

 Test Files  2 failed (2)
      Tests  2 failed (2)

price.ts を変更したことで、直接依存する price.test.ts と推移的に依存する cart.test.ts の 2 件が選択され、独立した greet.test.ts はスキップされています。エージェントが次の行動を決めるのに必要な情報(どこが・なぜ失敗したか)だけが残る形です。AI コーディングエージェント内での実行を Vitest が検知した場合はこのレポーターが自動で有効になりますが、スクリプトに明示しておけば検知に依存せず確実に適用できます。

package.json の変更は全件実行にフォールバックする

依存パッケージの更新は影響範囲を静的に追跡できないため、全テストを回すべきです。これは設定不要で、Vitest の forceRerunTriggers のデフォルト値 ['**/package.json/**', '**/vitest.config.*/**', '**/vite.config.*/**'] がこの挙動を提供します。これらのパターンに一致するファイルが変更に含まれる場合、--changed は差分の推移的追跡ではなく全テスト実行にフォールバックします。手元で確かめる場合は、package.json に空行を 1 つ加えて npm run test:changed を実行すると 3 ファイルすべてが走ることを確認できます。

テストが JSON や CSV などのデータファイルを読み込んでいる場合、それらは静的インポートではないため追跡できません。forceRerunTriggers にパターンを追加して、全件実行へフォールバックさせます。

vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    forceRerunTriggers: [
      '**/package.json/**',
      '**/vitest.config.*/**',
      '**/fixtures/**', // テストが読み込むデータファイル
    ],
  },
})

Jest で同じことを実現する場合

本記事のローカル・CI 検証は Vitest で行いましたが、Jest にも同等の機能が標準で用意されています。CLI オプションの対応は次のとおりです(Jest CLI Options)。

目的 Vitest Jest
git 差分に関連するテストだけ実行 vitest --changed jest --onlyChanged
特定ファイル起点で関連テストを実行 vitest related <file> jest --findRelatedTests <file>
ブランチ差分を基準に実行 vitest --changed origin/main jest --changedSince=origin/main
対象テストが無くても成功終了 (既定で exit 0 --passWithNoTests

Jest 固有の注意点として、関連テストが 1 件も見つからない場合の挙動に差があります。--onlyChanged は差分なしの状態で正常終了しますが、--findRelatedTests にテストのないファイルを渡すと終了コード 1 で失敗します。lint-staged やエージェントから機械的に呼び出す用途では --passWithNoTests を付けておきます。また --changedSince は、現在のブランチが指定ブランチから分岐している場合、対象がローカルで加えた変更のみになる点に注意が必要です(詳細は公式 CLI ドキュメントを参照してください)。

組み込みパターン 1: AI コーディングエージェント

エージェントが迷わず最小実行を選べるように、package.json に専用スクリプトを定義します。

{
  "scripts": {
    "test": "vitest --run",
    "test:changed": "vitest --changed --run --reporter=agent"
  }
}

その上で、CLAUDE.md(または各エージェントの指示ファイル)に実行ルールを書いておきます。

## テスト実行ルール
- コード変更後の確認は `npm run test:changed` を実行する
- package.json または設定ファイルを変更した場合のみ `npm test` で全件実行する

このルールが実際に効くかを、Claude Code で確認しました。テストコマンド名を含めず「src/price.ts の税率を 10% から 8% に変更して、影響がないか確認してください」とだけ指示したところ、エージェントは price.ts を変更した後、CLAUDE.md の指示に従って npm run test:changed を自ら選択して実行しました。どのコマンドを使うかは指示していません。

Claude Code が CLAUDE.md の指示に従い npm run test:changed を自律選択している様子

このときの test:changed の出力が次です。10%→8% の変更で price.test.tscart.test.ts の期待値と食い違うため 2 件とも失敗し、独立した greet.test.ts はスキップされています。エージェントは「影響がある」ことを、変更に関係する 2 ファイルだけを走らせて確認できています。

> vitest --changed --run --reporter=agent

 RUN  v4.1.8 /Users/hidetaka/sandboxes/vitest-run-changed-test

 ❯ src/price.test.ts (1 test | 1 failed) 3ms
   × taxIncluded adds 10% tax 3ms
 ❯ src/cart.test.ts (1 test | 1 failed) 3ms
   × cartTotal sums tax-included prices 3ms

 Test Files  2 failed (2)
      Tests  2 failed (2)

期待値を 8% 側に修正すれば、同じ選択のまま 2 件が成功し、サマリーのみが出力されます。

> vitest --changed --run --reporter=agent

 RUN  v4.1.8 /Users/hidetaka/sandboxes/vitest-run-changed-test

 Test Files  2 passed (2)
      Tests  2 passed (2)

CLAUDE.md に 1 行のルールを書くだけで、エージェントがコード変更後に全件実行ではなく変更関連実行を選ぶことが確認できました。Jest を使う場合は test:changedjest --onlyChanged --passWithNoTests に置き換えます。

組み込みパターン 2: pre-commit hook

lint-staged は対象ファイルのパスをコマンド末尾に渡すため、related / --findRelatedTests にそのまま引数として届きます。ステージングされたファイルに関連するテストだけがコミット前に実行され、全テストスイートの完了を待たずにコミットできます。

.lintstagedrc.js(Vitest)
export default {
  '*.{js,ts}': 'vitest related --run',
}

Jest を使う場合は jest --findRelatedTests --passWithNoTests を指定します。

組み込みパターン 3: CircleCI でのプルリクエスト時の選択的実行

プルリクエストのワークフローでは、main ブランチとの差分に関連するテストだけを実行します。以下は実際に動作を確認した設定です。

.circleci/config.yml
version: 2.1
jobs:
  test-changed:
    docker:
      - image: cimg/node:22.14
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: npm ci
      - run:
          name: Fetch base branch
          command: git fetch origin main
      - run:
          name: Run tests related to changes
          command: npx vitest --changed origin/main --run --reporter=agent
  test-all:
    docker:
      - image: cimg/node:22.14
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: npm ci
      - run:
          name: Run all tests
          command: npm test
workflows:
  pr-checks:
    jobs:
      - test-changed:
          filters:
            branches:
              ignore: main
  main-full:
    jobs:
      - test-all:
          filters:
            branches:
              only: main

ポイントは 2 つあります。

  1. git fetch origin main を明示的に実行する: --changed origin/main が差分を計算するには、ジョブ環境内に比較先の ref が必要です
  2. main ブランチでは全件実行する: main への push では origin/main との差分がほぼ無く、--changed の対象が空になりえます。そのため main では test-allnpm test)で全件を回し、セーフティネットにします

実際の挙動

この設定を実リポジトリで動かし、次の 2 パターンを確認しました。greet.ts とそのテストだけを変更した feature ブランチと、それを main にマージした後の実行です。

ブランチ ワークフロー ジョブ 実行されたテストファイル
feature ブランチ pr-checks test-changed greet.test.ts の 1 件のみ
main(マージ後) main-full test-all 3 件すべて

PR 側では変更した greet.ts に関連する greet.test.ts の 1 件だけが実行され、price.test.ts / cart.test.ts はスキップされました。main 側では 3 件すべてが実行されています。選択実行が意図どおり効いていることは、この「1 件 vs 3 件」というファイル数の差で確認できます。

なお、本記事のサンプルはテストが数 ms 規模のため、CI の所要時間差は npm ci などのセットアップが支配的で、選択実行そのものの短縮効果は現れません。時間短縮が意味を持つのは、実行に時間のかかるテストを多数抱えた実プロジェクトです。本サンプルで示せるのは「変更に関係するファイルだけを正しく選択する」という選択の正確さです。

スクリーンショット 2026-07-03 14.07.38.png

変更検知が効かないケース

静的解析ベースである以上、追跡できない依存があります。

// 追跡可能: 静的インポート
import { taxIncluded } from './price'

// 追跡不可能: 動的インポート
const module = await import(getModulePath())

// 追跡不可能: ファイルシステム経由の読み込み
const config = fs.readFileSync('./config.json')

これらに依存するテストは変更検知から漏れます。対策は 2 段構えです。

  • Vitest なら forceRerunTriggers に該当ファイルのパターンを追加して全件実行へフォールバックさせる
  • CI で main ブランチ・定期実行の全テストをセーフティネットとして維持する

モノレポで「アプリ A の変更時はアプリ A のテストだけ」といったプロジェクト単位の制御が必要になったら、Nx や Turborepo の affected 検出が次の選択肢になります。ただし単一パッケージのリポジトリであれば、本記事で確認した標準機能で十分です。

まとめ

AI コーディングエージェントにとって、テスト出力はコンテキストウィンドウという有限のリソースを消費する入力です。Vitest が agent レポーターを標準搭載したように、「エージェントに渡す情報を絞る」ことはツールエコシステム側でも前提になりつつあります。

本記事のサンプルプロジェクトは 10 分程度で再現できます。手元で推移的な依存追跡の挙動を確認したら、package.json にスクリプトを 1 行足し、CLAUDE.md に実行ルールを 1 行書くところから始めてください。Claude Code はその 1 行のルールに従い、変更後に変更関連実行を自律的に選びました。ローカルのエージェント、pre-commit hook、CircleCI の PR ワークフローと段階的に広げつつ、main ブランチと定期実行の全テストをセーフティネットとして残す。この構成が、選択の正確さと検出力を両立する現実的な落としどころです。

参考リソース

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?