Claude Media
Claude CodeでLWCのJestテストをhooksで自動実行する設定

Claude CodeでLWCのJestテストをhooksで自動実行する設定

LWCのファイルをClaude Codeが編集した直後に、PostToolUse hookでsfdx-lwc-jestを走らせ、失敗を終了コード2でClaudeに返す設定とスクリプトを示します。

Lightning Web Components(LWC)をClaude Codeに直させるとき、編集のたびにJestテストを走らせて失敗を本人に読ませる最短の方法が、PostToolUse hookです。sfdx-lwc-jestを呼ぶシェルスクリプトを1本置き、失敗したら終了コード2で止めると、テストの出力がそのままClaudeの次の入力になります。

LWCのJestをhooksに載せる全体像

仕組みは3つの部品でできています。

手順

編集から修正までの流れ

  1. 1

    Claudeがファイルを編集する

    EditツールまたはWriteツールがLWCの.js・.html・.cssを書き換えます。

  2. 2

    PostToolUse hookがスクリプトを起動する

    編集が成功した直後に、設定したコマンドへ標準入力でJSONが渡されます。

  3. 3

    スクリプトがsfdx-lwc-jestを実行する

    変更ファイルに関係するテストだけを走らせます。

  4. 4

    失敗なら終了コード2で返す

    標準エラーの内容がClaudeに表示され、次のターンで修正に使われます。

PostToolUseは、ツールが成功したあとに動くイベントです。入力にはツールへの引数(tool_input)と結果(tool_response)が入っていて、ファイル系ツールのパスは常に絶対パスで届きます。パスから「LWCの下のファイルか」を判定できるので、スクリプト側で対象を絞れます。

テストを走らせる側のsfdx-lwc-jestは、Salesforce DXプロジェクトでLWCのJestを動かすためのパッケージです。@lwc/jest-preset由来のtransformerやresolverを設定済みで、jest.config.jsを書かなくても動く作りになっています。--より後ろの引数はJestにそのまま渡されます。この仕様が、変更ファイルだけをテストする絞り込みの土台になります。

前提と準備

必要なものは次のとおりです。

  • Salesforce DXプロジェクト(force-app/main/default/lwc/にコンポーネントがある構成)
  • @salesforce/sfdx-lwc-jestがdevDependenciesに入っていること
  • hookスクリプトがJSONを読むためのjq

導入コマンドとpackage.jsonのスクリプトは、パッケージのREADMEにある形で足せます。

npm install --save-dev @salesforce/sfdx-lwc-jest
{
  "scripts": {
    "test:unit": "sfdx-lwc-jest",
    "test:unit:watch": "sfdx-lwc-jest --watch",
    "test:unit:coverage": "sfdx-lwc-jest --coverage"
  }
}

まず手動でnpm run test:unitが通ることを確かめます。hookは手動で通るコマンドを自動化するだけなので、ここが赤いままでは何も始まりません。

プロジェクトのルートにjest.config.js・jest.config.mjs・jest.config.cjsのいずれかがあると、sfdx-lwc-jestは組み込みの設定を注入しません。READMEが示す上書きの書き方は、既定設定を取り込んで広げる形です。

const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
    ...jestConfig,
    // 独自の設定をここに足す
};

独自の設定ファイルを置いているプロジェクトでも、hookのコマンドは同じです。

変更ファイルのテストだけを走らせるスクリプト

Jestには--findRelatedTestsがあり、渡したソースファイルをカバーするテストだけを実行します。公式の説明では、コミット前フックで必要最小限のテストを回す用途に向くオプションです。--passWithNoTestsを足すと、該当テストが無いときも成功扱いになります。

.claude/hooks/lwc-jest.shとして保存する例です。

#!/bin/bash
# PostToolUse: LWC のファイルが変わったら関連する Jest テストを実行する
f=$(jq -r '.tool_input.file_path // empty')
 
case "$f" in
  */lwc/*/*.js|*/lwc/*/*.html|*/lwc/*/*.css) ;;
  *) exit 0 ;;
esac
 
cd "$CLAUDE_PROJECT_DIR" || exit 0
 
comp=$(echo "$f" | sed -E 's#.*/lwc/([^/]+)/.*#\1#')
 
case "$f" in
  *.js)
    out=$(npx sfdx-lwc-jest -- --findRelatedTests "$f" --passWithNoTests 2>&1)
    ;;
  *)
    # html/css はコンポーネント単位でテストを選ぶ
    out=$(npx sfdx-lwc-jest -- "/lwc/$comp/" --passWithNoTests 2>&1)
    ;;
esac
code=$?
 
if [ "$code" -ne 0 ]; then
  echo "LWC の Jest テストが失敗しました (component: $comp)" >&2
  echo "$out" | tail -n 60 >&2
  exit 2
fi
exit 0

実行権限を付けておきます。

chmod +x .claude/hooks/lwc-jest.sh

要点は4つあります。

  1. 対象の絞り込み: パスが/lwc/<名前>/配下の.js・.html・.css以外なら、何もせずexit 0で抜けます。
  2. 作業ディレクトリ: CLAUDE_PROJECT_DIRはセッションを始めたプロジェクトルートです。スクリプトはこの変数でルートへ移動してからnpxを呼びます。
  3. .jsと.htmlの分け方: .jsは--findRelatedTestsで依存関係をたどります。.htmlと.cssは、Jestの位置引数(テストファイルに当てる正規表現)でそのコンポーネントのテストを選びます。
  4. 出力の切り詰め: tail -n 60で長い失敗ログを末尾に絞ります。スタックトレースの山でコンテキストを埋めないための調整です。

--findRelatedTestsは依存の逆引きなので、共通のユーティリティを書き換えたときは、それを読み込む複数コンポーネントのテストがまとめて動きます。テストが1件も無いコンポーネントを触っても、--passWithNoTestsのおかげで失敗扱いにはなりません。

settings.jsonにPostToolUseを登録する

プロジェクト共有の.claude/settings.jsonに、次のように書きます。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "if": "Edit(**/lwc/**)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/lwc-jest.sh",
            "timeout": 180
          },
          {
            "type": "command",
            "if": "Write(**/lwc/**)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/lwc-jest.sh",
            "timeout": 180
          }
        ]
      }
    ]
  }
}

matcherはツール名に対する絞り込みで、英数字と|だけで書いた値は完全一致のリストとして扱われます。Edit|WriteはEditとWriteのどちらかに一致します。

ifはツール呼び出しの中身で絞る追加の条件です。権限ルールと同じ書式で、ファイル系ツールではEdit(**/src/**)のように任意の深さのディレクトリを指定できます。1つのifに書けるルールは1つだけで、||のような結合はありません。EditとWriteの両方を対象にするため、ハンドラーを2つに分けています。スクリプト内でもパスを判定しているので、ifを外しても動作は変わりません。ifは不要な起動を減らす役目です。

timeoutは秒単位で、command型の既定は600秒です。LWCのテストは通常数十秒以内に収まるため、180秒にして暴走を早めに切る設定にしています。実際の所要時間はプロジェクトによって違うので、手動実行の時間を測ってから決めます。

失敗がClaudeに届く仕組みと、届かない場面

PostToolUseで終了コード2を返すと、標準エラーがClaudeに表示されます。ただしこのイベントでは、終了コード2でも編集は取り消されません。ツールはすでに実行済みだからです。止めたいのではなく「失敗を見せて直させる」ための使い方になります。

終了コードごとの扱いは次のとおりです。

終了コードPostToolUseでの扱い
0PostToolUseでの扱い成功。標準出力は通常ログに残り、Claudeには表示されない
2PostToolUseでの扱い標準エラーをClaudeに表示。ツールは実行済みで取り消されない
それ以外PostToolUseでの扱いブロックしないエラー。標準エラーの1行目が通知に付くだけで、Claudeへの表示は保証されない

「テストが落ちたのにClaudeが無反応」という症状の多くは、スクリプトが1で終了しているケースです。Jestの終了コードをそのまま返すと、失敗時は1になって届きません。上のスクリプトでexit 2へ明示的に読み替えているのはそのためです。

終了コードを使わずにJSONで返す方法もあります。終了コード0のまま、標準出力に次のようなJSONを1つ出力します。

{
  "decision": "block",
  "reason": "LWC の Jest テストが失敗しました。出力を読んで修正してください。"
}

decision: "block"を返すと、reasonがツール結果の横に追加されます。元の出力もそのまま残ります。長いテストログを渡したいなら、hookSpecificOutputのadditionalContextにまとめる方法もあります。どの形でも、標準出力にはJSONだけを出す必要があります。ほかの文字を混ぜるとプレーンテキストか解析失敗として扱われるためです。スクリプトでjq -nを使って組み立てると、引用符の崩れを避けられます。

Bashツール経由でファイルを書き換えた場合は、この設定では動きません。公式ドキュメントも、Edit|Writeに一致するPostToolUseはBashコマンドや外部プロセスによる書き換えでは動かないと説明しています。Claudeがsed -iでLWCを直したときの取りこぼしが心配なら、matcherを*にしてスクリプト内でgit status --porcelainを見る方式にします。

実行が長いときはasyncRewakeに切り替える

LWCプロジェクトが大きくなり、1回のテストに分単位でかかるようになったら、同期実行は編集のたびにClaudeを待たせます。そのときはasyncRewakeを使います。hookをバックグラウンドで走らせ、終了コード2で終わったときだけClaudeを起こす指定です。

{
  "type": "command",
  "if": "Edit(**/lwc/**)",
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/lwc-jest.sh",
  "asyncRewake": true,
  "timeout": 300
}

バックグラウンド実行では、async: trueと違ってasyncRewakeにはtimeoutが引き続き効きます。標準エラー(空なら標準出力)がシステムリマインダーとしてClaudeに見えるので、失敗時の動きは同期版と同じです。その間にClaudeは次の編集へ進めます。

注意点もあります。非同期のhookは、結果が次のターンで届きます。Claudeが先に別のコンポーネントを触り終えてから失敗が返ることもあります。どのコンポーネントの失敗かを後から追えるよう、スクリプトのメッセージにコンポーネント名を入れてあります。同期と非同期の使い分けは、Claude Code hooksでテストを自動実行する方法でPostToolUseとStopの違いとあわせて解説しています。

CLAUDE.mdにテストの扱いを書いておく

hookは失敗を伝えるだけで、直し方までは指示しません。放っておくと、Claudeがテストの期待値を書き換えて通す、スナップショットを-uで更新して終わらせる、といった近道を選ぶことがあります。sfdx-lwc-jestには-u(--updateSnapshot)が用意されており、失敗したスナップショットをすべて再記録できるため、手が伸びやすい選択肢です。CLAUDE.mdに次のような規約を置いておきます。

## LWC のテスト
 
- LWC を編集すると PostToolUse hook が関連する Jest テストを自動実行する
- テストが失敗したら、まずコンポーネント側を直す。期待値の変更は仕様が変わったときだけ
- スナップショットの更新(`-u`)は、見た目の変更を意図したときに限る
- 失敗が直らないときは、原因の仮説を書いてから次の修正に進む

テストが無いコンポーネントを新しく足すときは、先にテストファイルを書かせると、hookが最初の1回目から効きます。既存のコードにテストを足す作業は、カバレッジの穴を埋める手順がそのまま使えます。

つまずきやすいところ

@wireや外部コンポーネントの解決で落ちる

テストが「コンポーネントが見つからない」で落ちるときは、hookではなくJest側の設定が原因です。lightning名前空間のスタブはパッケージに入っていますが、網羅的ではありません。ローカルのlwcディレクトリにない別名前空間のコンポーネントは、moduleNameMapperで自作のモックに対応づけます。READMEの例では^foo/fancyButton$のようなキーで、jest.config.jsに書きます。@wireのデータを用意する場合は@salesforce/wire-service-jest-utilのAPIを使います。

手元では通るのにhookだと落ちる

作業ディレクトリと環境変数の違いを疑います。スクリプト冒頭でCLAUDE_PROJECT_DIRへcdしているのは、そのためです。npxがプロジェクトのローカルなnode_modulesを見つけられる位置で動く必要があります。Nodeのバージョンも要確認です。バージョンマネージャーを使っている環境では、hookの子プロセスで手元のシェルと同じNodeが選ばれているかを確かめます。

失敗したり不安定だったりして原因が追えない

テストが毎回同じ結果にならない場合は、hookで繰り返し実行しても失敗ログが増えるだけです。原因の切り分けはフレーキーテストの原因調査の手順に切り替えます。

hookが動いたかどうかを確かめたい

/hooksメニューで登録内容を確認できます。起動したかを見るだけなら、スクリプトの先頭に一時的なechoを入れて標準エラーに書き、Claudeにファイルを1つ直させてみる方法が手早いです。終了コード0の標準エラーはClaudeに表示されないため、動作確認には終了コード2を使うか、デバッグログを見ます。

CIとの役割分担

hookで回すのは、直前の編集に関係する範囲だけです。プロジェクト全体のテストとカバレッジは、PRごとにCIで走らせます。CIが落ちたときの修正をClaudeに任せる流れは、CIのテスト失敗を自動修正する手順にあります。手元では素早い関連テスト、リモートでは全件、と段を分けると、待ち時間と見逃しのバランスが取りやすくなります。

まとめ

LWCのJestをhooksに載せる設定の核心は、スクリプトが失敗時に終了コード2を返す1点です。Jestの終了コード1のままではClaudeに届きません。sfdx-lwc-jest -- --findRelatedTests <ファイル> --passWithNoTestsで関連テストだけを走らせれば、編集ごとの待ち時間は短く保てます。重くなったらasyncRewakeへ、ルールの抜け道はCLAUDE.mdで塞ぐ、という順で整えていくと運用が安定します。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks
  2. github.com/salesforce/sfdx-lwc-jest
  3. jestjs.io/docs/cli
この記事を共有:XはてブLinkedIn