PesterによるPowerShellのテスト整備 ── 運用スクリプトを壊しにくくする実務の型

· 更新日: · · PowerShell, Pester, Windows, テスト, 自動化, CI, 既存資産活用

更新履歴(3件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
GitHub Actions と Azure Pipelines の完全な設定例を追加しました。あわせて社内環境で Install-Module がつまずく原因と対処、スクリプトの種類ごとにどこを単体で見るかの対応表、カバレッジ設定の例を追加しています。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589838)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「PesterによるPowerShellのテスト整備 ── 運用スクリプトを壊しにくくする実務の型」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589838 https://staging.comcomponent.com/blog/2026/06/08/000-pester-powershell-test-maintenance/

DOI(最新版)
10.5281/zenodo.21589838
DOI(この版)
10.5281/zenodo.21732849

1. 最初に押さえるべきこと

PowerShell スクリプトは、最初は小さな作業の自動化として始まります。

ファイルを集める。 ログを検索する。 CSV を作る。 古いファイルを移動する。 サービスの状態を確認する。

どれも数十行なら、目で見て動作確認できます。しかし、実務で使い続けているうちに、こうした変更が入ります。

  • 対象フォルダーを増やす
  • 除外条件を追加する
  • CSV の列を変える
  • 削除前にアーカイブする
  • タスクスケジューラや CI から実行する
  • エラー時に通知する

この段階になると、「一度手元で動いた」だけでは足りません。

PowerShell の怖さは、便利さと表裏一体です。 読み取りだけなら気軽に試せますが、削除、移動、上書き、サービス再起動、権限変更のような処理は、少しの条件ミスが事故につながります。

そこで使いたいのが、PowerShell 用のテストフレームワークの Pester です。この記事では、Pester の機能を網羅するのではなく、すでにある PowerShell スクリプトを実務で壊しにくくするための「テスト整備」の進め方を整理します。

PowerShell のテストは、きれいなコードを書くためだけのものではありません。変更前に不安を減らし、変更後に根拠を持って確認するための道具です。

なお、この記事に登場するコードは、Invoke-Pester で実行できるサンプル一式(テスト対象のスクリプト、Pester テスト、CI 用の実行スクリプト)として GitHub で公開しています。

pester-powershell-test-maintenance - komurasoft-blog-samples (GitHub)

この記事の知識マップ

この記事はPowerShellの運用スクリプトをPester v5で壊しにくくする実務手順を、テスト対象を守る仕組みの関係として整理します。Pesterは$TestDriveで本物のファイルに触れずにファイル操作を検証し、Mockで削除や通知のような危険操作を実行せず呼び出し条件だけを確認します。削除対象を選ぶ関数を危険な変更処理より先に切り出してテストし、日時をGet-Dateの直接呼び出しではなく引数として渡すことでテストの不安定性を防ぎます。GitHub ActionsやAzure PipelinesはPesterの実行を自動化しますが、Windowsに同梱される旧バージョンのPesterとは別に明示的な再インストールが必要で、社内プロキシやTLS設定がInstall-Moduleによるモジュール導入を妨げることもあります。

PesterによるPowerShellテスト整備の知識マップPesterがPowerShellスクリプトを検証するテストフレームワークであり、TestDriveでファイル操作を、Mockで削除等の危険操作を安全に検証し、対象選定関数を危険操作より先に固め、日時を引数化してテストの不安定性を防ぎ、GitHub ActionsやAzure PipelinesでCIとして自動化する一方、Windows同梱の旧バージョンとの違いや社内プロキシによる導入阻害に注意する必要があることを示す図利用する利用する利用する利用する利用する軽減する推奨される対応軽減する自動化する自動化する両立しない両立しないで確認できる利用する前提とするより先に行うべき防止する利用するPesterPowerShell$TestDrivePesterのモック(Mock)Pesterのタグ(Tag)PesterのCode CoverageSupportsShouldProcess(-WhatIf)危険操作(削除・移動・上書きなど)GitHub ActionsAzure Pipelines社内プロキシ/TLSによるモジュール導入の阻害Install-ModuleによるPowerShell Galleryからの導入閉域ネットワーク対象選定を切り出した関数日付の引数化(Now引数)テストの不安定性(flaky test)

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全18件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. Pesterで何を守るのか

Pester を入れると、すべてが自動で安全になるわけではありません。まず決めるべきなのは、「何をテストで守るか」です。

PowerShell の運用スクリプトでは、この4つを優先すると効果が出やすいです。

守りたいこと テストで見る内容
条件判定 どのファイル、行、ユーザー、サービスを対象にするか
出力の形 CSV に出す列名、戻り値のプロパティ、件数
危険操作の手前 削除・移動・停止の対象が意図通りか
外部依存 ファイルシステム、API、コマンド実行、日時、環境変数の扱い

特に最初にテストしたいのは、削除処理そのものではなく、削除対象を選ぶ処理です。

たとえば、古いログを削除するスクリプトなら、いきなり Remove-Item をテストするのではなく、先に「どのログが対象として選ばれるか」をテストします。

この分け方をすると、テストしやすくなります。

対象を集める関数
  ↓
対象を確認・記録する処理
  ↓
移動・削除・通知などの変更処理

PowerShell のテスト整備は、既存スクリプトをいきなり大改造することではありません。まずは、危険な処理の前にある判断部分を関数として切り出し、その戻り値を Pester で確認します。

3. バージョンをそろえる

この記事では Pester v5 を前提にします。

古い Windows 環境では、あらかじめ Pester が入っていても v3 系であることがあります。既存環境に入っているものをそのまま使うのではなく、まずバージョンを確認します。

Get-Module Pester -ListAvailable |
  Sort-Object Version -Descending |
  Select-Object Name, Version, Path

新しく入れる場合は、PowerShell Gallery からインストールします。

Install-Module -Name Pester -Scope CurrentUser -Force -SkipPublisherCheck
Import-Module Pester
Get-Module Pester

-SkipPublisherCheck を付けているのは、Windows に同梱されている古い Pester が Microsoft の署名付きで入っているためです。これがないと、発行元が違うという理由でインストールが止まります。

なお、社内端末ではこのコマンドがそのままでは通らないことがあります。プロキシ、TLS 1.2、NuGet プロバイダーが典型的な原因です。対処は 17 章にまとめました。

チームで使う場合は、手元の開発端末、ビルドサーバー、タスク実行環境で Pester のバージョンがずれていないかを確認します。

PowerShell のテストでよくある混乱は、コードの問題ではなく、テストランナーのバージョン差から起きます。

特に、古い記事や社内メモには Pester v4 以前の書き方が残っていることがあります。新しく整備するなら、v5 の書き方に寄せた方が後で読みやすくなります。

4. ファイルの置き場所を決める

Pester では、テストファイルを *.Tests.ps1 という名前にするのが一般的です。

最小構成はこうです。

scripts/
  Get-OldLogFile.ps1
  Get-OldLogFile.Tests.ps1

もう少し規模が大きい場合は、srctests を分けます。

src/
  public/
    Get-OldLogFile.ps1
    Remove-OldLogFile.ps1

tests/
  public/
    Get-OldLogFile.Tests.ps1
    Remove-OldLogFile.Tests.ps1

どちらでも構いません。大事なのは、規則を決めることです。

  • 1つの関数に対して1つのテストファイルを置く
  • テストファイル名には .Tests.ps1 を付ける
  • テスト対象の読み込み方をそろえる
  • 単体テストと結合テストを混ぜすぎない

最初は、対象の .ps1 とテストの .Tests.ps1 を隣に置く形で十分です。

5. 最小のテストを動かす

まず、単純な関数 Get-OldLogFile.ps1 を用意します。

function Get-OldLogFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [int] $Days = 30,

        [string] $Filter = '*.log',

        [datetime] $Now = (Get-Date)
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
        throw "Folder not found: $Path"
    }

    $limit = $Now.AddDays(-1 * $Days)

    Get-ChildItem -LiteralPath $Path -Filter $Filter -File |
        Where-Object { $_.LastWriteTime -lt $limit } |
        Sort-Object -Property LastWriteTime |
        Select-Object FullName, Name, Length, LastWriteTime
}

ここでは、テストしやすくするために、$Now を引数として受け取れるようにしています。

関数の中で毎回 Get-Date を直接使うと、テスト実行日によって結果が変わります。日付を引数にしておけば、「2026年6月1日時点で30日より古いファイル」という条件を固定してテストできます。

次に、テストを Get-OldLogFile.Tests.ps1 に書きます。

BeforeAll {
    . $PSScriptRoot\Get-OldLogFile.ps1
}

Describe 'Get-OldLogFile' {
    BeforeEach {
        $script:Root = Join-Path $TestDrive 'logs'
        New-Item -ItemType Directory -Path $script:Root -Force | Out-Null

        $oldLog = Join-Path $script:Root 'old.log'
        $newLog = Join-Path $script:Root 'new.log'
        $oldTxt = Join-Path $script:Root 'old.txt'

        Set-Content -LiteralPath $oldLog -Value 'old log' -Encoding UTF8
        Set-Content -LiteralPath $newLog -Value 'new log' -Encoding UTF8
        Set-Content -LiteralPath $oldTxt -Value 'old text' -Encoding UTF8

        (Get-Item -LiteralPath $oldLog).LastWriteTime = [datetime]'2026-05-01T00:00:00'
        (Get-Item -LiteralPath $newLog).LastWriteTime = [datetime]'2026-05-31T00:00:00'
        (Get-Item -LiteralPath $oldTxt).LastWriteTime = [datetime]'2026-05-01T00:00:00'
    }

    It '指定日数より古い .log ファイルだけを返す' {
        $result = Get-OldLogFile `
            -Path $script:Root `
            -Days 30 `
            -Now ([datetime]'2026-06-01T00:00:00')

        $result | Should -HaveCount 1
        $result[0].Name | Should -Be 'old.log'
    }

    It '存在しないフォルダーでは失敗する' {
        { Get-OldLogFile -Path (Join-Path $TestDrive 'missing') } |
            Should -Throw
    }
}

実行します。

Invoke-Pester -Output Detailed .\Get-OldLogFile.Tests.ps1

ここで使っている $TestDrive は、Pester が用意するテスト用の一時領域です。本物の C:\Logs や共有フォルダーを使わずに、テストの中だけで作ったファイルを使えます。ファイル操作を伴う PowerShell スクリプトでは、まず $TestDrive を使う習慣をつけると安全です。

6. テスト名は仕様として書く

Pester の It に書く文字列は単なる説明ではなく、後から読む人にとっては小さな仕様書になります。

たとえば、こういう名前は少し弱いです。

It 'works' {
    # ...
}

何が動けばよいのか分かりません。

実務では、条件と期待結果を名前に入れる方が読みやすくなります。

It '指定日数より古い .log ファイルだけを返す' {
    # ...
}

It 'ちょうど期限日のファイルは対象にしない' {
    # ...
}

It '存在しないフォルダーでは失敗する' {
    # ...
}

良いテスト名は、失敗したときに効きます。CI のログにこう出たとき、何が壊れたのかすぐ分かります。

[-] Get-OldLogFile.ちょうど期限日のファイルは対象にしない

テスト名は、未来の自分へのメモです。

7. 境界条件を1つ足す

先ほどの Get-OldLogFile は、この条件で古いファイルを判定しています。

$_.LastWriteTime -lt $limit

-lt なので、期限日と同じ日時のファイルは対象外です。

この判断は小さいですが、実務では重要です。「30日より古い」なのか、「30日前を含む」なのかで対象件数が変わるからです。

境界条件をテストに追加します。

It 'ちょうど期限日のファイルは対象にしない' {
    $border = Join-Path $script:Root 'border.log'
    Set-Content -LiteralPath $border -Value 'border log' -Encoding UTF8
    (Get-Item -LiteralPath $border).LastWriteTime = [datetime]'2026-05-02T00:00:00'

    $result = Get-OldLogFile `
        -Path $script:Root `
        -Days 30 `
        -Now ([datetime]'2026-06-01T00:00:00')

    $result.Name | Should -Not -Contain 'border.log'
}

テストは、たくさん書けばよいものではありません。しかし、日付、数値、件数、権限、ファイル名のパターンのように、境界がある処理はテストの価値が高いです。

8. 戻り値の形を固定する

PowerShell スクリプトでは、戻り値の形がいつの間にか変わることがあります。

最初は FileInfo をそのまま返していた。 途中で Select-Object を入れた。 さらに CSV 用に列名を変えた。

このような変更は後続処理に影響するので、戻り値のプロパティをテストしておくと、予期しない変更に気づけます。

It '後続処理で使うプロパティを返す' {
    $result = Get-OldLogFile `
        -Path $script:Root `
        -Days 30 `
        -Now ([datetime]'2026-06-01T00:00:00')

    $propertyNames = $result[0].PSObject.Properties.Name

    $propertyNames | Should -Contain 'FullName'
    $propertyNames | Should -Contain 'Name'
    $propertyNames | Should -Contain 'Length'
    $propertyNames | Should -Contain 'LastWriteTime'
}

CSV 出力やレポート作成に渡す関数では、値だけでなく列名も仕様です。

「動いた」だけでなく、「次の処理が期待する形で返っている」ことを確認します。

9. 削除処理は対象選定と分ける

次に、削除処理を考えます。悪い例から見ます。

Get-ChildItem C:\Logs -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
    Remove-Item -Force

短くて便利ですが、テストしにくいです。対象選定と削除が1本のパイプラインでつながっているため、どこを確認すればよいか分かりにくくなります。

実務では、こう分けます。

function Remove-OldLogFile {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [int] $Days = 30,

        [datetime] $Now = (Get-Date)
    )

    $targets = Get-OldLogFile -Path $Path -Days $Days -Now $Now

    foreach ($target in $targets) {
        if ($PSCmdlet.ShouldProcess($target.FullName, 'Remove old log file')) {
            Remove-Item -LiteralPath $target.FullName -Force
        }
    }
}

ここでは SupportsShouldProcess を付けて、関数側で -WhatIf を受けられるようにしています。

Remove-OldLogFile -Path C:\Logs -Days 30 -WhatIf

削除系の PowerShell 関数では、可能な限り -WhatIf で予行できる形にしておく方が安全です。

10. Mockで危険操作を置き換える

Pester では Mock を使って、実際のコマンド実行を置き換えられます。

削除処理のテストで本当に Remove-Item を実行する必要はありません。

呼ばれるべき場面で呼ばれたか。 呼ばれてはいけない場面で呼ばれていないか。

そこを見れば十分です。

Remove-OldLogFile.Tests.ps1 の例です。

BeforeAll {
    . $PSScriptRoot\Get-OldLogFile.ps1
    . $PSScriptRoot\Remove-OldLogFile.ps1
}

Describe 'Remove-OldLogFile' {
    It '古いログファイルに対して Remove-Item を呼ぶ' {
        Mock Get-OldLogFile {
            [pscustomobject]@{
                FullName      = 'C:\Logs\old.log'
                Name          = 'old.log'
                Length        = 10
                LastWriteTime = [datetime]'2026-05-01'
            }
        }

        Mock Remove-Item {}

        Remove-OldLogFile `
            -Path 'C:\Logs' `
            -Days 30 `
            -Now ([datetime]'2026-06-01')

        Should -Invoke Remove-Item `
            -Times 1 `
            -Exactly `
            -ParameterFilter { $LiteralPath -eq 'C:\Logs\old.log' }
    }

    It 'WhatIf では Remove-Item を呼ばない' {
        Mock Get-OldLogFile {
            [pscustomobject]@{
                FullName      = 'C:\Logs\old.log'
                Name          = 'old.log'
                Length        = 10
                LastWriteTime = [datetime]'2026-05-01'
            }
        }

        Mock Remove-Item {}

        Remove-OldLogFile `
            -Path 'C:\Logs' `
            -Days 30 `
            -Now ([datetime]'2026-06-01') `
            -WhatIf

        Should -Invoke Remove-Item -Times 0
    }
}

このテストでは、Get-OldLogFileRemove-Item もモックしているので、本物の C:\Logs\old.log は存在しなくても構いません。見ているのは、Remove-OldLogFile の判断です。

  • 対象があれば Remove-Item を呼ぶ
  • -WhatIf のときは Remove-Item を呼ばない
  • 呼ぶときは意図したパスを渡す

危険な処理ほど、実行そのものではなく、呼び出し条件をテストする方が安全です。

11. Mockを使いすぎない

Mock は便利ですが、使いすぎるとテストの価値が落ちます。すべてをモックすると、実際の PowerShell の動きから離れすぎるからです。

目安はこのあたりです。

処理 おすすめ
日付 引数で固定する
ファイル作成 $TestDrive を使う
削除・移動 Mock-WhatIf で確認する
Web API 呼び出し Invoke-RestMethod などを Mock する
メール送信・通知 送信コマンドを Mock する
CSV の読み書き 小さな実ファイルを $TestDrive に作る

ファイルの読み書きまで全部モックすると、実際の文字コード、改行、列名の問題を見逃すことがあります。

一方で、削除、通知、外部 API、サービス停止のような処理は、本当に実行しない方がよいです。

「本物を使う場所」と「モックする場所」を分けます。

12. 既存スクリプトをテストしやすく直す

Pester を入れると、既存スクリプトの書き方も少し変わります。ただし、最初から大きな設計変更は不要で、まずはこの程度の手直しで十分です。

直す前

$limit = (Get-Date).AddDays(-30)

Get-ChildItem C:\Logs -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt $limit } |
    Remove-Item -Force

直した後

function Get-OldLogFile {
    param(
        [string] $Path,
        [int] $Days = 30,
        [datetime] $Now = (Get-Date)
    )

    $limit = $Now.AddDays(-1 * $Days)

    Get-ChildItem -LiteralPath $Path -Filter *.log -File |
        Where-Object { $_.LastWriteTime -lt $limit }
}

function Remove-OldLogFile {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string] $Path,
        [int] $Days = 30,
        [datetime] $Now = (Get-Date)
    )

    Get-OldLogFile -Path $Path -Days $Days -Now $Now |
        ForEach-Object {
            if ($PSCmdlet.ShouldProcess($_.FullName, 'Remove old log file')) {
                Remove-Item -LiteralPath $_.FullName -Force
            }
        }
}

変更点は大きくありません。

  • 日付を引数にした
  • 対象選定を関数にした
  • 削除処理を別関数にした
  • SupportsShouldProcess を付けた

これだけでテストしやすくなります。

PowerShell のテスト整備では、設計論から入るよりも、「日付」「パス」「外部コマンド」「変更操作」を外から差し替えられるようにする方が効果的です。

13. テストの分類を決める

Pester では DescribeContextIt にタグを付けられます。

たとえば、速い単体テストと、実際の環境に触る結合テストを分けます。

Describe 'Get-OldLogFile' -Tag 'Unit' {
    It '指定日数より古い .log ファイルだけを返す' {
        # TestDrive を使う速いテスト
    }
}

Describe 'Log maintenance smoke test' -Tag 'Smoke' {
    It '実際のログフォルダーを読み取れる' {
        Test-Path -LiteralPath 'C:\Logs' | Should -BeTrue
    }
}

単体テストだけ実行します。

Invoke-Pester -TagFilter Unit

遅いテストや環境依存のテストを除外します。

Invoke-Pester -ExcludeTagFilter Slow, RequiresAdmin, Network

現場では、すべてのテストを毎回実行しようとすると続かないことがあります。

まずは、速くて副作用のないテストを標準にし、環境依存のテストはタグで分けて必要なタイミングで実行します。

スクリプトの種類ごとの対応表

「このスクリプトはどこまで単体テストで、どこから結合テストか」「そこで何を使うか」は、スクリプトの種類でだいたい決まります。

スクリプトの種類 単体テストで見るところ 使う仕組み 結合テストで見るところ
ファイル収集・対象選定 条件の境界。何日前まで含むか、拡張子で絞れているか $TestDrive に小さな実ファイルを作る 本物のフォルダーで件数が想定どおりか
CSV・JSON の入出力 列名、列の順序、文字コード、改行 $TestDrive に実ファイルを読み書きする 受け渡し先のシステムで実際に開けるか
削除・移動・上書き 削除対象の選定結果と、-WhatIf での予行 Mock Remove-Item / Mock Move-Item 検証環境で1回だけ実行して結果を確認
サービス・プロセス操作 状態から次の操作を決める判定ロジック Mock Get-Service などで状態を作る 検証機で実際に起動・停止する
外部 API・通知・メール送信 組み立てたリクエストや本文の中身 Mock Invoke-RestMethod / Mock Send-MailMessage ステージング宛てに1回だけ送る
日付・期間で判断する処理 境界日の判定。当日、前日、うるう日 日付を引数で固定する(Mock しない)
レジストリ・システム設定の読み取り 読み取った値をどう解釈するか Mock Get-ItemProperty 実機で実際の値を確認する

見方は 2 つです。

  1. 単体テストは「自分たちの判断」を見る。 標準コマンドが正しく動くことは確認しません(19 章)。
  2. 危険操作は単体テストで実行しない。 削除、移動、送信、通知は Mock で置き換え、実行そのものは結合テストで、回数を絞って確認します。

日付だけは Mock ではなく引数で固定する点に注意してください。Get-Date をモックすると、同じスクリプト内の別の用途の日付取得まで巻き込みます。

14. CIで動かす

Pester は手元で実行するだけでも役に立ちますが、チームでスクリプトを管理するなら、CI で実行できるようにしておくと安心です。

たとえば、tools/Invoke-ProjectTests.ps1 のようなファイルを用意します。

$ErrorActionPreference = 'Stop'

# 環境に古い Pester が残っていることがあるので、v5 以上を明示して読み込む
Import-Module Pester -MinimumVersion 5.0.0

$config = New-PesterConfiguration

$config.Run.Path = @(
    Join-Path $PSScriptRoot '..\tests'
)

$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'

$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'JUnitXml'
$config.TestResult.OutputPath = Join-Path $PSScriptRoot '..\test-results.xml'

$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = @(
    Join-Path $PSScriptRoot '..\src'
)
$config.CodeCoverage.OutputPath = Join-Path $PSScriptRoot '..\coverage.xml'

Invoke-Pester -Configuration $config

CI 側では、このスクリプトを実行します。

pwsh -NoProfile -File .\tools\Invoke-ProjectTests.ps1

ポイントは、CI 固有の設定をテストファイルの中に書きすぎないことです。

テストファイルは仕様を書く場所です。 CI 用の出力形式、カバレッジ、終了コードなどは、実行用スクリプトにまとめると整理しやすくなります。

この実行用スクリプトができていれば、CI 側の設定はどのサービスでも短く済みます。

GitHub Actions の場合

.github/workflows/pester.yml を置きます。

name: pester

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: windows-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install Pester v5
        shell: pwsh
        run: |
          Install-Module -Name Pester -MinimumVersion 5.0.0 `
            -Scope CurrentUser -Force -SkipPublisherCheck

      - name: Run Pester
        shell: pwsh
        run: ./tools/Invoke-ProjectTests.ps1

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: pester-results
          path: |
            test-results.xml
            coverage.xml

押さえておきたいのは次の点です。

  • Pester は明示的に入れる。 Windows には Pester v3 系が同梱されているため、入れ直さないと v5 の書き方が動きません。
  • -SkipPublisherCheck を付ける。 同梱の Pester は Microsoft の署名が付いているため、これがないと発行元が違うという理由でインストールが止まります。
  • shell: pwsh を明示する。 GitHub ホステッドの Windows ランナーでは既定が pwsh ですが、セルフホストランナーでは PowerShell 7 がなければ Windows PowerShell にフォールバックします。明示しておくと環境差で悩まずに済みます。
  • if: always() で結果ファイルを回収する。 テストが落ちたときこそ結果を見たいので、失敗時も実行されるようにします。

ジョブを失敗させるのは $config.Run.Exit = $true です。これがあると、テストが失敗したときに Invoke-Pester が 0 以外の終了コードで終わり、ステップも失敗します。逆にこれを入れ忘れると、テストが赤でもジョブが緑になります。

Azure Pipelines の場合

azure-pipelines.yml はこうなります。

trigger:
  - main

pool:
  vmImage: 'windows-latest'

steps:
  - task: PowerShell@2
    displayName: 'Install Pester v5'
    inputs:
      pwsh: true
      targetType: 'inline'
      script: |
        Install-Module -Name Pester -MinimumVersion 5.0.0 `
          -Scope CurrentUser -Force -SkipPublisherCheck

  - task: PowerShell@2
    displayName: 'Run Pester'
    inputs:
      pwsh: true
      filePath: 'tools/Invoke-ProjectTests.ps1'

  - task: PublishTestResults@2
    displayName: 'Publish test results'
    condition: always()
    inputs:
      testResultsFormat: 'JUnit'
      testResultsFiles: 'test-results.xml'
      failTaskOnFailedTests: true

PublishTestResults@2 が受け付ける形式は JUnitNUnitVSTestXUnitCTest です。実行用スクリプトで $config.TestResult.OutputFormat = 'JUnitXml' にしているので、ここでは JUnit を選びます。出力形式を変えるときは、両方をそろえて変更してください。

failTaskOnFailedTests: true を入れておくと、結果ファイルに失敗が含まれている時点でタスクが失敗します。既定は false で、その場合は結果を表示するだけで通ってしまいます。

15. カバレッジは目標ではなく地図として見る

Pester ではコードカバレッジも出せます。ただし、最初から数字を追いかけすぎない方がよいです。カバレッジは、テストの品質そのものではないからです。

たとえば、削除対象の抽出関数を1回呼べば、その行は通ったことになります。しかし、境界条件や除外条件が確認されていなければ、実務上の安心にはつながりません。

カバレッジはこう使います。

  • まったく通っていない関数を見つける
  • 重要なのにテストがない分岐を見つける
  • 変更頻度の高いスクリプトから優先順位をつける
  • CI でテスト実行の証跡を残す

数字を上げることよりも、「大事な判断がテストされているか」を見ます。

取得は New-PesterConfigurationCodeCoverage 側で設定します。

$config = New-PesterConfiguration

$config.Run.Path = @('.\tests')

$config.CodeCoverage.Enabled = $true

# 測定対象。テストファイルではなく、テスト対象のスクリプトを指定する
$config.CodeCoverage.Path = @('.\src')

# 出力形式。既定は JaCoCo で、CI のカバレッジ表示に取り込みやすい
$config.CodeCoverage.OutputFormat = 'JaCoCo'
$config.CodeCoverage.OutputPath = '.\coverage.xml'

# 目標値。下回ってもテストは失敗しないので、あくまで目安として置く
$config.CodeCoverage.CoveragePercentTarget = 75

Invoke-Pester -Configuration $config

指定する CodeCoverage.Path は、テストファイルではなくテスト対象のスクリプトです。ここを .\tests にしてしまうと、テストコード自身のカバレッジを測ることになり、数字は高いのに中身がない状態になります。

出力形式は既定が JaCoCo です。CoverageGutters を選ぶと、エディター上で行ごとの通過状況を見られる形式になります。CI に取り込むなら JaCoCo のままで問題ありません。

CoveragePercentTarget は目安の数字であって、下回っても実行が失敗するわけではありません。カバレッジを合否条件にしたくなったときこそ、この章の冒頭に戻ってください。数字を満たすためのテストは、たいてい壊れたときに何も教えてくれません。

16. 既存スクリプトへの導入順序

既存の PowerShell 資産に Pester を入れる場合、いきなり全体をテスト対象にしない方が進めやすいです。おすすめの順番を挙げます。

1. 失敗すると困るスクリプトを選ぶ

最初は、こういうスクリプトが向いています。

  • 削除、移動、上書きを含む
  • 毎日または毎月動く
  • 手順書に残っているが属人化している
  • 過去に条件ミスがあった
  • 出力 CSV が他の業務に使われている

便利だけれど壊れると困るものから始めます。

2. 読み取り部分だけを関数に切り出す

最初にテストするのは、変更処理ではなく読み取り処理です。

ログを読む
対象を絞る
件数を数える
CSV 用の形に整える

この部分は $TestDrive でテストしやすく、事故も起きにくいです。

3. 日付とパスを外から渡す

日付やパスが固定されていると、テストしにくくなります。

# 避けたい
$root = 'C:\Logs'
$limit = (Get-Date).AddDays(-30)

テストしやすい形です。

param(
    [string] $Path,
    [datetime] $Now = (Get-Date)
)

値を外から渡せるだけで、テストの安定性が大きく上がります。

4. 危険操作を最後にする

削除や移動は、最後にまとめます。

対象を作る
  ↓
対象をログに残す
  ↓
-WhatIf で確認する
  ↓
実行する

テストでも同じ順番で確認します。

17. よくあるつまずき

症状 原因 対処
手元では通るが CI で落ちる カレントディレクトリが違う $PSScriptRoot を基準にする
日によって結果が変わる Get-Date を直接使っている -Now のような引数を用意する
テストで本物のファイルを消しそうになる 実フォルダーを使っている $TestDriveMock を使う
Mock が効かない モジュール境界やスコープが違う -ModuleName や読み込み方法を確認する
どこまでテストすべきか分からない 仕様が関数に分かれていない 対象選定、整形、変更処理に分ける
テストが遅い 外部サービスやネットワークに触っている 単体テストでは外部依存を Mock する
テスト名を見ても分からない It 'works' のような名前になっている 条件と期待結果を名前に入れる

Pester の問題に見えて、実際にはスクリプトの構造が原因になっていることも多いです。

テストしにくい部分は、だいたい運用でも壊れやすい部分です。

Install-Module でつまずく(社内環境に多い)

3 章の Install-Module は、インターネットに直接出られる環境ならそのまま通ります。一方、情シスが管理する社内端末では、ここで止まることがよくあります。

症状 原因 対処
PowerShell Gallery に接続できない 社内プロキシを経由していない -Proxy-ProxyCredential を指定する
「基になる接続が閉じられました」などで失敗する Windows PowerShell 5.1 の既定プロトコルに TLS 1.2 が入っていない セッションで TLS 1.2 を有効にしてから実行する
NuGet プロバイダーの導入を聞かれて止まる Windows PowerShell 同梱の PowerShellGet 1.0.0.1 のままになっている 先に NuGet プロバイダーを入れる
そもそも外部に出られない 閉域ネットワーク 別端末で Save-Module して持ち込むか、社内リポジトリを Register-PSRepository で登録する

上の 3 つは、この順番で実行すると通ることが多いです。

# 1. TLS 1.2 を有効にする(Windows PowerShell 5.1 で必要。PowerShell 7 では不要)
[Net.ServicePointManager]::SecurityProtocol =
    [Net.ServicePointManager]::SecurityProtocol -bor
    [Net.SecurityProtocolType]::Tls12

# 2. NuGet プロバイダーを入れる(対話プロンプトで止まらないように先に入れる)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force

# 3. プロキシ経由で Pester を入れる
$proxyUri = 'http://proxy.example.local:8080'
$proxyCredential = Get-Credential -Message 'プロキシ認証'

Install-Module -Name Pester -MinimumVersion 5.0.0 `
    -Scope CurrentUser -Force -SkipPublisherCheck `
    -Proxy $proxyUri -ProxyCredential $proxyCredential

TLS 1.2 の設定は、そのセッションでしか効きません。毎回入力するのが面倒なら、プロファイルスクリプトに書いておきます。

プロキシが認証不要なら -ProxyCredential は省略できます。プロキシの URL が分からない場合は、情シスに「PowerShell Gallery(www.powershellgallery.com)へ出るときのプロキシ設定」を確認してください。

閉域で外部に出られない場合は、インターネットに出られる端末で次を実行し、できたフォルダーをそのまま持ち込みます。

# 持ち出し側
Save-Module -Name Pester -MinimumVersion 5.0.0 -Path 'D:\modules'

持ち込み側では、D:\modules\Pester$env:USERPROFILE\Documents\WindowsPowerShell\Modules などモジュールの検索パスへ配置します。配置先は $env:PSModulePath で確認できます。

この手順を最初にドキュメント化しておくと、担当者が増えたときの立ち上がりが速くなります。テスト整備が進まない理由が「Pester が入らない」だった、というのは実際によくあります。

18. テスト整備で決めておきたいルール

チームで PowerShell スクリプトを管理するなら、細かい書き方よりも先にルールを決めます。

たとえばこういうルールです。

  • テストファイルは *.Tests.ps1 にする
  • テスト対象は $PSScriptRoot から読み込む
  • ファイル操作のテストは $TestDrive を使う
  • 削除、移動、通知、API 呼び出しは原則 Mock する
  • 日付は固定できるように引数化する
  • Describe または ItUnitSmokeRequiresAdmin などのタグを付ける
  • CI では Unit を標準実行する
  • 変更系関数には可能な限り SupportsShouldProcess を付ける
  • 過去の不具合は再発防止テストとして残す

ルールは多すぎると守られません。最初は、この3つだけでも十分です。

TestDrive を使う
日付を固定する
危険操作は Mock する

この3つを守るだけで、PowerShell のテストはかなり安定します。

19. どこまでテストしないかも決める

テスト整備では、「何をテストするか」と同じくらい「何をテストしないか」も重要です。

たとえば、このあたりは単体テストで無理に確認しすぎない方がよいです。

  • Windows 自体の Get-ChildItem が正しく動くこと
  • Remove-Item が本当にファイルを削除すること
  • PowerShell の標準コマンドの内部仕様
  • 社外 API が常に応答すること
  • ネットワーク共有が常に使えること

テストすべきなのは、自分たちの判断です。

  • どの条件で対象にするか
  • どのパスを渡すか
  • どの列を出力するか
  • 失敗時にどう扱うか
  • 危険操作を予行できるか

標準コマンドを信じるところと、自分たちのロジックを守るところを分けます。

20. まとめ

PowerShell は、日常作業をすばやく自動化できる便利な道具です。しかし、実務で長く使うスクリプトは、少しずつ責任が重くなります。最初は自分だけが使う1行コマンドだったものが、やがて毎日動く運用処理になり、他の人の作業や業務データに影響するようになります。

Pester によるテスト整備は、その変化に合わせてスクリプトを守るための作業です。

ポイントを並べておきます。

  • まず対象選定をテストする
  • 日付やパスを外から渡せるようにする
  • ファイル操作は $TestDrive に閉じ込める
  • 削除、移動、通知、API 呼び出しは Mock する
  • 変更系関数は -WhatIf で予行できるようにする
  • テスト名を仕様として読めるようにする
  • CI では速くて副作用のないテストから回す

PowerShell の安全な運用は、いきなり大きな仕組みを入れることではありません。

小さな関数に分ける。 小さなテストを書く。 危険な処理の前に確認できる形にする。

この積み重ねで、PowerShell は「便利だけれど少し怖いスクリプト」から、「変更しても確認できる業務ツール」に近づきます。

参考リンク

関連する記事

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Pesterで最初に何をテストすればよいですか?
削除処理そのものではなく、削除対象を選ぶ処理(対象選定)から始めます。運用スクリプトでは「条件判定」「出力の形」「危険操作の手前」「外部依存」の4つを優先すると効果が出やすいです。既存スクリプトをいきなり大改造する必要はなく、危険な処理の前にある判断部分を関数として切り出し、その戻り値をPesterで確認するところから始めます。
PesterのTestDriveとは何ですか?
$TestDriveは、Pesterが用意するテスト用の一時領域です。本物のC:\Logsや共有フォルダーを使わずに、テストの中だけで作ったファイルを使ってファイル操作を検証できます。ファイル操作を伴うPowerShellスクリプトのテストでは、まず$TestDriveを使う習慣をつけると、本物のファイルを誤って消すような事故を防げます。
PesterのMockはどこまで使うべきですか?
削除・移動、Web API呼び出し、メール送信・通知のような実行してはいけない処理はMockで置き換え、日付は引数で固定し、ファイル作成やCSVの読み書きは$TestDriveに小さな実ファイルを作る方が向いています。すべてをモックすると実際のPowerShellの動きから離れすぎて、文字コードや改行、列名の問題を見逃すことがあるため、「本物を使う場所」と「モックする場所」を分けるのがポイントです。
手元では通るPesterテストがCIで落ちるのはなぜですか?
よくある原因はカレントディレクトリの違いで、テスト対象の読み込みを$PSScriptRoot基準にすると解消できます。日によって結果が変わる場合はGet-Dateを直接使っているのが原因なので、-Nowのような引数で日付を固定します。Mockが効かない場合はモジュール境界やスコープの違いを疑い、-ModuleNameや読み込み方法を確認します。Pesterの問題に見えて、実際はスクリプトの構造が原因のことも多いです。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る