Skip to main content
モッドの自動テストを書いて、シェルから claude plugin test で実行できます。テストはフックが処理するイベントを発生させ、フックが何をしたかをチェックするため、セッションに到達する前に問題を見つけることができます。最初の例は モッドを作成する のモッドをテストします。

テストを書く

テストはモッドを読み込み、Claude Code が行うようにイベントをフックを通して送信し、セッション、サインイン、ネットワークなしでフックが何をしたかをチェックします。テストはシェルから claude plugin test で実行し、各テストファイルはテストキット(claude-code/testing モジュール内のテストライブラリ)をインポートします。 各テストファイルに .test.ts で終わる名前(例:first-mod.test.ts)を付け、プラグインディレクトリ内のどこかに保存します。すべてのテストファイルには少なくとも 1 つの test() が必要です。そうでないと、declares no test(): nothing ran で実行が失敗します。テストファイルはモッド自体のファイルと兄弟の .ts ヘルパーをインポートできるため、ゲームのルールなどのプレーン関数をキットなしでユニットテストできます。 このテストは 2 つのツール呼び出しを発生させ、モッドを作成する の /tally コマンドを実行し、返信が両方をカウントしていることをチェックします。最初の行は スタブ で、Claude Code の代わりにツール呼び出しに答えます。first-mod/tests/first-mod.test.ts として保存します:
first-mod/tests/first-mod.test.ts
シェルで first-mod ディレクトリからテストを実行します:
出力は各テストとそれが成功したかどうかを名前で示し、実行ごとに異なるタイミングを表示します:
各 $.tool.call はモッドの tool.call フックを通過し、カウントに 1 を追加してスタブに呼び出しを渡しました。ls は実行されず、ファイルは読み込まれませんでした。$.command.run はモッドの command.run フックに移動し、answer はそのフックが返したオブジェクトです。 テストが失敗すると、コマンドはステータス 1 で終了するため、CI で機能します。独自のモッドがそれを実行するシェルで読み込めない場合、claude plugin test: hooks modules are turned off で始まる行と理由を出力し、ステータス 1 で終了します。

Claude Code が答えるものをスタブする

テストではモデル、ストア、またはツールが実行されないため、モッドが Claude Code の回答を期待する場所では、テストはスタブで回答を提供します。テスト関数はそのために 2 つの引数を受け取ります:
  • $: テスト独自の $ で、Claude Code が立つ場所に立ちます。これはフックが受け取る mods API ではありません。各メソッドは同じ名前のイベントを発生させ、モッドのフックを通して送信し、結果に解決します:$.tool.call({ tool: 'Bash', command: 'ls' }) は tool.call を発生させます。$.command.run、$.prompt.submit、$.session.start、$.turn.complete は同じように機能し、$.classic.Stop と他の $.classic メソッドは 設定フックイベント を発生させます。テストは ui.close などの mods API 呼び出しを直接発生させることはできません。モッドを通してトリガーします。例えば、ペインを閉じるボタンを押します。
  • on: スタブを登録するために呼び出します。スタブは Claude Code の代わりに答えるフックです。mods API 呼び出しの $. なしでスタブに名前を付けます。そのため、store.get として登録されたスタブはモッドの $.store.get に答えます。モッドが $.model.complete または $.store.get を呼び出すと、スタブが回答を提供します。
この例はモデル呼び出しをスタブします。フックは grader という名前のモッドに属し、文をモデルに送信して返信が PASS で始まるかどうかを報告する /grade コマンドを処理します。ファイルはテスト中のフックのみを保持するため、モッドは モッドを作成する のように plugin.json と hooks.json も必要です。セッションで /grade と入力するには、モッドは コマンドを登録 する必要があります:
grader/hooks/register.js
このテストはモデル呼び出しをスタブして、フックが成功した返信で何をするかをチェックします:
grader/tests/grader.test.ts
テストは成功します。フックの reply は value の下のオブジェクトで、その text は PASS で始まるためです。他のブランチをチェックするには、スタブが FAIL で始まる text を返す 2 番目のテストを追加し、Try again を期待します。 mods API 呼び出しのスタブは value フィールドを持つオブジェクトを返します。これはモッドで呼び出しが解決するものを保持します:{ value: 7 } は $.store.get を 7 に解決させます。turn.step または tool.call などの Claude Code のイベントのスタブは、そのイベント独自の結果({ result: 'ok' } など)を返します。$.session.send と $.prompt.fill はイベントの結果もテーブルが示すように取ります。スタブが返すものを調べる は各一般的な名前がどの形式を取るかを示します。2 つのエラーはスタブが間違っているか不足していることを意味します。失敗したテストの出力には the engine reported: で始まるブロックが含まれ、各エラーがそこに表示されます:
  • returned neither { value } nor { deny }: mods API 呼び出しのスタブが裸の値を返した
  • no implementation for の後に名前が続く:モッドがその呼び出しを行い、スタブがそれに答えない
キットはまた、メモリ内モックをエクスポートします。これはネームスペース全体に答えます。mock.clock(on) は $.clock に答え、mock.store(on, { count: 7 }) はそれらのエントリで始まるストアから $.store に答え、mock.env(on, { CI: 'true' }) はそれらの変数から $.env.get に答えます。mock.clock はモッククロックを返し、テストはそれを進めるため、タイマーのテストは待機しません。mock.store は何も返さないため、モッドが何を保存したかをチェックするには、描画テスト が行うように 2 つの store スタブを自分で書きます。

テストキットのルールに従う

テストキットには独自のルールがいくつかあり、1 つを破ると新しいテスト作成者が最初に遭遇するエラーが生成されます:
  • $ の最初の呼び出しの前にすべてのスタブを登録します。 その後に on を呼び出すと、on("ui.render") after the test first called $ などのエラーがスローされます。
  • session.start は単独では実行されません。 各テストはモジュールが新しく読み込まれた状態で開始され、フックは呼び出されないため、モジュールレベルの変数は初期値を保持します。フックが session.start が設定するものに依存する場合、最初にそれを発生させます:
    2 番目のスタブは、session.start フック(例えば チュートリアル のもの)が行う $.command.register 呼び出しに答えます。それなしでは、その呼び出しは no implementation for command.register で拒否され、キットはフックをスキップするため、フック内の呼び出しの後の何も実行されません。テストはその時点で失敗しません。スキップされたフックは、後のチェックが失敗した場合にのみ the engine reported: の下にリストされます。
  • next(e) を返すフックにはスタブが必要です。 例えば、Claude がアイドル状態の間は何も描画しないために next(e) を返す ui.render フックは、マウント が no implementation for ui.render で失敗します。プレーンデータとして要素を返すスタブを登録します:
    スタブが登録されると、マウントが成功し、ui.find({ type: 'Text' }) はフックが next(e) を返すたびにその要素を返します。
  • turn.step のスタブは非同期ジェネレータです。テストはストリームを最後まで読んで結果を取得します:
    ループが終了すると、result はスタブが返したオブジェクトで、モッドの turn.step フックが変更する機会を持った後です。ここで result.answer は 'ok' です。
  • ツール呼び出しをツールの名前と引数をフィールドとして発生させます。例えば await $.tool.call({ tool: 'Bash', command: 'ls' })、{ result } を返す tool.call スタブを登録します。

スタブが返すものを調べる

モッドがテストで行う mods API 呼び出しはすべて、キットが自分で答える少数を除いて、スタブが答える必要があります:$.ui.invalidate と $.state 呼び出し。$.clock 呼び出しの場合、mock.clock(on) を使用するか、モッドの $.clock.now() は no implementation for clock.now で失敗します。 このテーブルはモッドが最も使用するものをリストします。最初の列はモッドが行う呼び出しまたは next(e) で渡すイベントです。2 番目はその名前の下で on に渡す関数です。そのため、$.store.get 行は on('store.get', ($, e) => ({ value: saved.get(e.key) })) になります。スタブ内の '...' は入力するテキストをマークします: expect には toBe、toEqual、toMatch、toMatchObject、toContain、toBeDefined、toBeUndefined、toThrow のアサーションがあり、それらのいずれかの前に .not があります。

タイマーをテストする

タイマーで作業を実行するモッドには、テストが制御するクロックが必要です。そのため、テストは待機する代わりに時間を前に進めることができます。const clock = mock.clock(on) は 0 で開始し、テストが移動するときのみ移動するモッククロックを返します。別の時間で開始するには、ミリ秒で渡します。例えば mock.clock(on, { now: 5000 }) のように。クロックには次のメソッドがあります: このフックは countdown という名前のモッドに属し、秒数を取る /countdown コマンドを処理し、1 秒の $.clock.every タイマーを開始し、ゼロでトーストを表示します。grader と同様に、ファイルはテスト中のフックのみを保持し、コマンドを登録しません:
countdown/hooks/register.js
このテストは /countdown 3 を実行し、モッククロックを移動するため、3 秒間の動作をチェックします。3 秒待つ必要はありません:
countdown/tests/countdown.test.ts
最初の expect はトーストが早く来ないことを示し、2 番目はそれが 1 回来ることを示します。各 advance は期限が来たタイマーが実行された後に解決するため、次の行のチェックはそれらの効果を見ます。

描画をテストする

テストはモッドの レンダリングサイト の 1 つを描画し、要素を押し、入力し、見つけることができます。$.ui.mount はサイトをモッドの ui.render フックを通して描画し、それぞれのメソッドを持つハンドルを返します。1 つのテストで複数のアプリをカバーするには、surface をアプリに設定して描画します。このテストは タブを使用してペインを構築する からペインを開き、タブを切り替え、ボタンを押し、ターミナルと Desktop アプリのカウントをチェックします:
hello-tabs/tests/hello-tabs.test.ts
シェルで hello-tabs ディレクトリから claude plugin test を実行します。テストは両方のアプリがカウント行を描画し、モッドが 2 を保存したときに成功します。最初のアプリから 2 番目のアプリへのカウントは、両方のマウントが同じ読み込まれたモジュールを使用するため、引き継がれます。 $.ui.mount が返すハンドルには次のメソッドがあり、モッドが与えた key で要素をアドレス指定します: 各メソッドはハンドラーが完了した後に解決するため、次の行で結果をチェックできます。props を Claude Code がそのサイトに渡すものに設定します。レンダリングサイトテーブル は各サイトの props をリストし、ビルドのタイプ はそれらのタイプを持ちます。 描画テストはフックが返すツリーをチェックし、そのアプリに対して有効かどうかをチェックします。アプリがそれをどのように描画するかはチェックしないため、実際のセッションで新しいレイアウトを見てください。

/clear の後に描画をテストする

各テストはすべての $.state 値がデフォルトで開始します。これは /clear がそれらを残す方法です。モッドが次に何をするかをテストするには、session.start をスキップし、source: 'clear' で classic.SessionStart を発生させ、モッドが描画するものをチェックします。 このテストは 保存された値を /clear の後に再度読み込む からモジュールをチェックします。描画をテストする からファイルに追加します。ここで PANE が定義されています。そのファイルの最初のテストはボタンがカウントを保存することを期待します。複数のセッションから保存する のボタンのように:
hello-tabs/tests/hello-tabs.test.ts
テストは、モッドの classic.SessionStart フックがペインが描画される前に保存された 7 を $.state にコピーしたときに成功します。モジュールにそのフックがない場合、ペインは Count: 0 を描画し、find は undefined を返し、テストは toBeDefined で失敗します。

他のモッドを判断するモッドをテストする

組織が prependPlugins にリストするモッドは、別のモッドが読み込まれる前にそれを拒否できます。1 つをテストするには、モッドのティアを設定し、テストに 2 番目のモッドを与えて、モッドが許可または拒否します:
  • tier: テストファイルの上部で 1 回呼び出します。例えば tier('prepend') のように。モッドを prepend、append、または builtin として読み込みます。これは モッドが実行される順序 でのその場所です。それなしでは、モッドは user として読み込まれます。
  • plugins: テスト本体の前にテストにオプションオブジェクトを渡します。その plugins 配列は、name と register 関数を持つ、インラインで書いたモッドを保持します。別の場所に 1 つを読み込むには、tier を追加します。
このテストファイルは 管理ページからのポリシーモッド を最初に読み込みます。ポリシーモッドがプロセスを開始するモッドを拒否し、そうでないモッドを許可することをチェックします:
acme-guard/tests/guard.test.ts
シェルで acme-guard ディレクトリから claude plugin test を実行します。両方のテストは管理ページが示すようにポリシーモッドで成功します。 キットはテストの最初の $ 呼び出しですべてのモッドを読み込みます。モッドが 1 つを拒否すると、その呼び出しはスローされ、メッセージは拒否されたモッド、それを拒否したモッド、および理由を名前で示します。2 番目のテストでは何も拒否されないため、reader はスタブに到達する前にツール呼び出しに答えます。

次のステップ