1
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?

PowerShellツール開発の落とし穴: 単体テストが通っても結合テストで爆死した話

Last updated at Posted at 2025-06-08

Qiita記事ドラフト: 「なぜ僕のPesterテストは全滅したのか?- PowerShellモジュールテストでハマった3つの罠と完全な解決策」

【プロローグ】勝利を確信した、その矢先に…

こんにちは!以前、「PowerShellでMarkdownパーサー自作してPester地獄から生還した話」という記事を書きました。あの後、単体テストは全てパスし、私は勝利を確信していました。

しかし、それは新たなる地獄の始まりでした。

リファクタリングを進め、テストを拡充するたびに、コンソールは再び赤いエラーメッセージで埋め尽くされました。

Tests completed in xxxms
Tests Passed: 0, Failed: 15, Skipped: 0, Inconclusive: 0, NotRun: 0

CommandNotFoundException, ParserError, ParameterBindingValidationException...。一度治したはずの病が次々と再発する。原因を一つ潰しても、また別のエラーが顔を出す。

この記事は、その「なぜか分からないがテストが全滅する」という泥沼から、どうやって根本原因を特定し、最終的にテストを安定させたかの全記録です。PowerShellでモジュールとPesterテストを扱うすべての人に、この経験が役立つことを願っています。

【第一の罠】モジュールはどこにいる? - Import-Module の迷宮

テストが失敗する最も基本的な原因は、「テスト対象の関数が見つからない (CommandNotFoundException)」ことでした。Import-Module を実行しているにも関わらず、です。

失敗したアプローチ: $env:PSModulePath の動的な変更

当初、私はテストスクリプト (MyCommonFunctions.Tests.ps1) の BeforeAll ブロックで、モジュールがあるディレクトリを $env:PSModulePath に一時的に追加し、モジュール名 (-Name 'MyCommonFunctions') でインポートしようとしました。

# 失敗したアプローチ
BeforeAll {
    $moduleDirectory = "..."
    $env:PSModulePath = "$($moduleDirectory);$($env:PSModulePath)"
    Import-Module -Name 'MyCommonFunctions' -Force
}

これは、ほとんどの環境で失敗しました。PowerShellのモジュール解決の仕組みは複雑で、$env:PSModulePath を動的に操作する方法は、環境依存性が高く非常に不安定です。

解決策: 絶対的なパスで、一度だけインポートする

根本的な対策は、全ての曖昧さを排除することでした。

  1. スクリプトの先頭で一度だけインポート: BeforeAllBeforeEach のようなブロックではなく、スクリプトの最上部で実行します。
  2. フルパスを直接指定: -Name パラメータではなく、.psd1 マニフェストファイルへの解決済みのフルパスを直接渡します。
  3. -Force オプション: 実行ごとにモジュールが再ロードされることを保証します。
  4. -ErrorAction Stoptry-catch: インポート自体が失敗した場合、テストを続行せずに即座に停止させ、問題に気づけるようにします。
# MyCommonFunctions.Tests.ps1 (完成版の冒頭)
try {
    # $PSScriptRoot (テストスクリプト自身の場所) を基準に相対パスを解決
    $modulePath = Join-Path $PSScriptRoot "../../src/powershell/Modules/MyCommonFunctions/MyCommonFunctions.psd1"
    Import-Module -Name $modulePath -Force -ErrorAction Stop
}
catch {
    Write-Error "テストの前提条件であるモジュールのインポートに失敗しました。パス: '$modulePath', Error: $($_.Exception.Message)"
    return # スクリプトを終了
}

# この後に Describe ブロックが続く...

教訓①: テストの依存関係は、曖昧な探索に頼らず、常に明示的かつ直接的に解決せよ。

【第二の罠】Pester構文の時限爆弾 - Ver5 との静かなる戦い

モジュールがロードされるようになっても、RuntimeException: Legacy Should syntax...ParameterBindingValidationException が頻発しました。

原因と解決策

  • Pester 5.x 構文の徹底: 原因は、テストスクリプト内に古いPester 3.x/4.x の構文 (Should -Be) と、新しいPester 5.x の構文 (Should Be) が混在していたことでした。中途半端な修正が、Pesterのパーサーを混乱させ、予期せぬ ParameterBindingValidationException を誘発していました。
    解決策: VS Codeの「ファイル内をすべて置換」機能を使い、Should -BeShould Be に、Should -BeNullOrEmptyShould BeNullOrEmpty に、機械的に一括変換しました。 これで構文エラーは完全に撲滅できました。

  • ファイルのエンコーディング: それでも残る不可解なエラーは、テストスクリプトファイルのエンコーディングが原因でした。ファイルを UTF-8 with BOM で保存し直すことで、Pester 5.x の動作が安定しました。特に日本語を含む環境では必須のようです。

  • VS CodeのPowerShellバージョン: 最終的に、VS Codeが使用するPowerShellを、システム標準の Windows PowerShell (5.1) から、別途インストールした PowerShell 7.xsettings.json で明示的に切り替えたことも、全体の安定に大きく寄与しました。

教訓②: テストフレームワークのバージョンアップは、破壊的変更を伴う。移行ガイドを熟読し、構文を完全に統一せよ。そして、自分のコードだけでなく、エディタや実行環境の設定も疑え。

【第三の罠】ロジックは正しい、でもテストは失敗する - 仕様書 vs テストケース

全ての実行時エラーが消え、ついにテストが中身の評価を始めると、今度は Expected ... But was ... というアサーション失敗の嵐に見舞われました。

原因と解決策

これは、最も時間を費やしたデバッグでした。

  • デバッグ出力の徹底: ConvertTo-AttributeObject 関数内に Write-Verbose を大量に仕込み、「何を入力し、どう解釈し、何を返したか」を徹底的に可視化しました。
  • 真実の発見: ログを分析すると、関数のロジックは仕様通りに動作していました。 問題は、テストケース側で用意した入力文字列(カンマの数)や、期待値(Should Be "..." の中身)が、仕様とズレていたことでした。
    • 例えば、仕様では13個の属性フィールドを定義しているのに、テストケースの入力文字列には12個しかフィールドがなく、最後の値が「コメント」ではなく「最終更新日」として解釈されていた、などです。
  • 解決策: テストケースの一つ一つを、仕様書と関数のデバッグ出力を元に、期待値を正確に修正していきました。ConvertTo-AttributeObject のロジック自体はほとんど変更する必要はありませんでした。

教訓③: テストが失敗した時、我々はつい「コードのバグ」を疑う。しかし、「テストの期待値の間違い」や「テストデータの仕様不整合」も同等に疑うべきである。

【結論】安定したテスト環境を手に入れて

この長く苦しい戦いの末、ついに15件のテストが全てパスする、安定したテスト環境を構築できました。

Tests completed in 419ms
Tests Passed: 15, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

この経験から得られた再発防止策は、今後のはい、承知いたしました。
Geminiが出力した「事実確認と再発防止策」の内容と、それ以前の経緯を踏まえ、私が最終的に提案するQiita記事のドラフトをリライトします。

リライトのポイント:

  • 読者層の再設定: 前回のドラフトは少し開発者向けすぎたかもしれません。今回は、**「PowerShellを業務で使っていて、もう少し高度なことに挑戦したいが、テストでつまずいている人」**をメインターゲットに設定し、より共感しやすく、実践的な内容を目指します。
  • ストーリー性の強化: 「Pester地獄」というキャッチーな言葉を開発における私の憲法となるでしょう。
  1. 仕様書を絶対的な正義とする。
  2. テストケースは仕様書から作る。
  3. ツールの入力には必ずバリデーションを入れる。
  4. 環境設定(PowerShellバージョン、エンコーディング)を最初に固める。
  5. 複雑な処理には、ためらわずに詳細なデバッグ出力を仕込む。

PowerShellとPesterは、正しく使えば非常に強力な開発・テストツールです。この記事が、過去の私のように「なぜか動かない」テストに悩む誰かの助けになることを願っています。


(付録として、最終版の MyCommonFunctions.Test.ps1 の主要部分や、settings.json の設定例などを掲載)

1
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
1
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?