Claude Media
Claude CodeでPowerShellのPesterテストを完了条件にしてスクリプトを書く

Claude CodeでPowerShellのPesterテストを完了条件にしてスクリプトを書く

Windowsの運用スクリプトをClaude Codeに書かせるとき、Invoke-Pesterの終了コードを完了条件にし、削除系の操作はMockと-WhatIfで試す手順を示します。

PowerShellスクリプトをClaude Codeに書かせるなら、完了の合図は「Pesterのテストが全部通ること」に置くのが手堅い方法です。Invoke-Pesterは失敗があれば非ゼロの終了コードで終われるので、Claudeが自分で実行して、赤が出たら直す往復を回せます。

ただし情シス向けの運用スクリプトには、ファイル削除やアカウント変更のような取り消せない操作が入りがちです。テストを書かせるだけでは足りません。破壊的な操作をMockに差し替え、-WhatIfの経路で呼ばれないことまで検証させる必要があります。この記事では、その構成と落とし穴を順に示します。

最初に潰す前提: Windows標準のPesterは3.4.0

Windows 10とWindows Server 2016以降には、Pester 3.4.0が最初から入っています。標準版はUpdate-Moduleでは更新できないため、最新版は並べて入れます。

Install-Module -Name Pester -Force -SkipPublisherCheck

-Forceが並べての導入、-SkipPublisherCheckが発行元証明書の違いを受け入れる指定です。標準版は署名元がMicrosoftで、新しい版はコミュニティ署名のため、指定なしだとエラーになることがあります。対応するPowerShellはWindows PowerShell 5.1とPowerShell 7.4以降です。

この確認を飛ばすと、Claudeが書いたテストは別の理由で落ちます。構文が3.4.0と現行版で違うためです。Claudeはエラーを見て、テストではなく実装を直し始めかねません。最初にGet-Module Pester -ListAvailableで入っている版を出し、結果をClaudeに見せておくと、この迷走を避けられます。

CLAUDE.mdに書くPester規約

Pester 6のドキュメントは、アサーションをShould-Beのようなハイフン形で書きます。v5までのShould -Be形は、設定Should.DisableV5で使えなくできます。どちらで書かせるかは、プロジェクトで入れた版に合わせて決め、CLAUDE.mdに固定します。

## PowerShellスクリプトの規約
- 関数は `scripts/<関数名>.ps1`、テストは同じ場所の `<関数名>.Tests.ps1`
- テストは Pester 6 の構文(`Should-Be` / `Should-Invoke`)で書く
- 関数ファイルはテストの BeforeAll でドットソースする
- 状態を変える関数は `[CmdletBinding(SupportsShouldProcess)]` を付け、
  実際の変更は `$PSCmdlet.ShouldProcess()` の内側に置く
- Remove-Item などはテストで必ず Mock し、`-WhatIf` では呼ばれないことを検証する
- 完了条件: `Invoke-Pester -Path ./scripts -Output Detailed -CI` が成功すること

最後の1行が要です。-CIは、テスト結果の出力を有効にし、実行後にプロセスを終了させるスイッチです。失敗があれば非ゼロで終了する設定Run.Exitが含まれるので、成否が終了コードに乗ります。

例題: 古いファイルを消す関数を、消さずに検証する

題材は、指定フォルダーにある30日より古いファイルを削除する関数です。次はClaudeに書かせる関数の形の一例です。

function Remove-StaleFile {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Path,
        [int]$OlderThanDays = 30
    )
    $limit = (Get-Date).AddDays(-$OlderThanDays)
    $targets = Get-ChildItem -Path $Path -File |
        Where-Object { $_.LastWriteTime -lt $limit }
    foreach ($file in $targets) {
        if ($PSCmdlet.ShouldProcess($file.FullName, 'Remove file')) {
            Remove-Item -LiteralPath $file.FullName
        }
    }
}

SupportsShouldProcessを指定すると、-WhatIfと-Confirmが自動で使えるようになります。-WhatIf付きで呼ぶと、ShouldProcess()は何をするかの説明を出して$falseを返すので、ifの中の削除は実行されません。

この関数をファイルに分けておけば、テストからドットソースで読み込めます。

BeforeAll {
    . $PSScriptRoot/Remove-StaleFile.ps1
}
 
Describe 'Remove-StaleFile' {
    BeforeEach {
        Mock Get-ChildItem {
            [pscustomobject]@{ FullName = 'C:\Temp\old.log'; LastWriteTime = (Get-Date).AddDays(-60) }
            [pscustomobject]@{ FullName = 'C:\Temp\new.log'; LastWriteTime = (Get-Date) }
        }
        Mock Remove-Item {}
    }
 
    It '-WhatIf では何も削除しない' {
        Remove-StaleFile -Path 'C:\Temp' -WhatIf
        Should-Invoke -CommandName Remove-Item -Times 0
    }
 
    It '30日より古いファイルだけを削除する' {
        Remove-StaleFile -Path 'C:\Temp'
        Should-Invoke -CommandName Remove-Item -Exactly -Times 1 `
            -ParameterFilter { $LiteralPath -eq 'C:\Temp\old.log' }
    }
}

Get-ChildItemとRemove-Itemの両方をMockしているので、このテストは実ファイルに触れません。Pesterは任意のPowerShellコマンドをMockできるため、削除系のコマンドレットも差し替えられます。

ここが肝: v5ではMockが本物にすり抜けることがある

Mockの-ParameterFilterが一致しなかった呼び出しを、Pester v5は黙って本物のコマンドに流しました。Pester v6はこの挙動をやめ、一致するフィルターも既定のMockも無ければ、本物を呼ばずにエラーにします。

削除系のコマンドにとって、この差は大きいです。たとえばClaudeが次のように書いたとします。

Mock Remove-Item {} -ParameterFilter { $LiteralPath -eq 'C:\Temp\old.log' }

v5では、フィルターに合わない別のパスの呼び出しが本物のRemove-Itemに届き、テスト中に実ファイルが消える余地があります。v6ならエラーで止まります。どちらの版でも、フィルターなしの既定Mock(Mock Remove-Item {})を先に置いておけば、すり抜けは起きません。上のテストがBeforeEachで既定Mockを置いているのは、このためです。

Claudeに任せる前に、次の2点をCLAUDE.mdか依頼文に入れておくと安全側に倒せます。

  • 破壊的コマンドのMockは、フィルターなしの既定Mockから書く
  • テストを走らせる前に、使うPesterの版を表示する

Claudeに走らせる: 権限と依頼文

テストの実行をClaudeに任せるには、Invoke-Pesterだけを許可し、削除コマンドは拒否します。PowerShellの権限ルールはBashと同じ形で書け、Remove-Itemをブロックする例が示されています。

{
  "permissions": {
    "allow": [
      "PowerShell(Invoke-Pester *)",
      "PowerShell(Get-Module *)"
    ],
    "deny": [
      "PowerShell(Remove-Item *)"
    ]
  }
}

ルールは別名も正規化して照合します。Remove-Itemのルールはrmやdelにも効きます。複合コマンドは、パイプや;で区切ったサブコマンドごとに判定され、全部が許可に合致して初めて通ります。

なお、WindowsでBash全体をdenyに入れると、PowerShellツールまで止まる挙動があります。この設定を書くときはBashのdenyで警告が出る条件を先に見ておくと、起動後に両方のツールが消える事態を避けられます。

依頼文は、完了条件と禁止事項を並べた形が書きやすいです。

scripts/Remove-StaleFile.ps1 を実装してください。
- 先に scripts/Remove-StaleFile.Tests.ps1 を書き、失敗を確認する
- Remove-Item と Get-ChildItem は既定の Mock を置いてから ParameterFilter を足す
- 実際のフォルダーに対して関数を実行しない(-WhatIf でも実行しない)
- Invoke-Pester -Path ./scripts -Output Detailed -CI が成功するまで繰り返す

止まる前に必ずテストを走らせる: Stop hookの例

Claudeが「終わりました」と言った時点でテストが赤なら、止めずに続けさせたい場面があります。Stopフックは、ターンの終わりにスクリプトを走らせ、終了コード2で停止を止められます。エラー出力に書いた理由は、Claudeへのフィードバックとして届きます。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "pwsh -NoProfile -File .claude/hooks/run-pester.ps1"
          }
        ]
      }
    ]
  }
}

run-pester.ps1は、Pesterを子プロセスで走らせて終了コードを見ます。-CIは実行後にプロセスごと終了させるため、同じセッションの中で呼ぶと、後ろの判定まで進みません。

pwsh -NoProfile -Command "Invoke-Pester -Path ./scripts -Output Detailed -CI"
if ($LASTEXITCODE -ne 0) {
    [Console]::Error.WriteLine('Pesterのテストが失敗しています。失敗したテストを直してください。')
    exit 2
}

Stopフックには上限があります。Claudeの操作を挟まずに8回連続でブロックすると、Claude Codeはフックを上書きして停止を許します。ドキュメントは、入力JSONのstop_hook_activeがtrueのときは早めに終了するスクリプトにするよう案内しています。テストが直らないまま同じ指摘を繰り返す事態を避けるための作りです。

フックの組み方そのものは、agent hookで「テストが通るまで止めない」を検証させる手順に詳しくあります。Pesterの代わりに別のテストランナーを使う場合も、骨格は同じです。

落とし穴

Mockだけのテストは、本物の削除が正しいことまでは保証しない

上のテストが確かめているのは、「どのパスに対してRemove-Itemが呼ばれるか」です。-LiteralPathの指定が正しいか、権限不足で削除に失敗したときの挙動が適切か、といった実コマンドの結果は見ていません。実フォルダーでの初回は、人が-WhatIf付きで実行して、出力された対象一覧を目で確かめてから、本番実行に進めます。

つまずきやすい5箇所

Pesterの入門に出てくる失敗や、Mockの仕様から拾える落とし穴を、症状と対処の形で並べます。

つまずき

Claudeが書いたテストで起きやすい失敗

  • CommandNotFoundException

    関数を別ファイルに移したのに、テストのBeforeAllでドットソースしていないと出ます。. $PSScriptRoot/<関数名>.ps1を入れます。直前のセッションに関数が残っていると、たまたま通ることがあるため、新しいPowerShellで走らせ直します。

  • Windows PowerShell 5.1のクラスMock

    PowerShellクラスのメソッドから呼ばれる関数のMockは、5.1までだと失敗することがあります。クラス定義がキャッシュされ、再定義されないためです。回避策として、テストを毎回新しいセッションで走らせる方法が案内されています。PowerShell 6以降では、この問題は知られていません。

  • 外部コマンドのMock

    curlのようなネイティブコマンドには、名前付きパラメーターがありません。-ParameterFilterの中では、$argsで引数を受けて判定します。

  • Mockの有効範囲

    Pester v5以降、Mockは置いたブロックの内側でのみ有効です。Describe直下に書いたつもりが、Itの中にしか効いていない、という食い違いに注意します。

  • モジュール内の呼び出し

    スクリプトモジュールの内部から呼ぶコマンドのMockには、追加の指定が要ります。モジュール化した運用スクリプトでは、-ModuleNameの指定を忘れないようにします。

運用スクリプトの完成度を決める判断

最後に、テストが通ったあとで人が見る箇所を決めておきます。

Pesterの緑が保証するのは、テストに書いた条件が満たされたことだけです。Claudeは、テストを通すためにテスト側を弱める可能性があります。差分を見るときは、実装よりもItブロックの変更と、Mockの中身が減っていないかを先に読みます。

情シス向けのスクリプトでは、次の3層に分けておくと運用しやすくなります。

層確かめること誰がやるか
Pesterの単体テスト確かめること呼び出しの有無と引数誰がやるかClaudeが自走
-WhatIfの実行確かめること対象一覧が意図どおりか誰がやるか人が目で確認
本番実行確かめること実際の結果誰がやるか人が実行

Claudeに任せるのは1行目までです。2行目以降は、削除や変更の実行権限を持つ人が踏む手順として残します。この線引きがあれば、Claudeに渡す権限はInvoke-Pesterの実行と、スクリプトファイルの編集に絞れます。

Windows側のシェル周りで、Claude Code Desktopの待ち時間などに困ったときは、PowerShellが毎回約154秒待たされる原因の切り分けも参考になります。

まとめ

Pesterの-CI付き実行を完了条件に置くと、PowerShellスクリプトの開発も、テストが赤なら直し、緑なら終わるループに乗せられます。運用スクリプトで効くのは、破壊的コマンドを既定のMockで受け、-WhatIfの経路で呼ばれないことを検証させる点です。本番に近い確認は、-WhatIfの出力を人が読む段で受け持ちます。

この記事を共有:XはてブLinkedIn