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

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

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

こんにちは！以前、「[PowerShellでMarkdownパーサー自作してPester地獄から生還した話](https://qiita.com/your-previous-article-url)」という記事を書きました。あの後、単体テストは全てパスし、私は勝利を確信していました。

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

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

```powershell
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'`) でインポートしようとしました。

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

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

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

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

1.  **スクリプトの先頭で一度だけインポート:** `BeforeAll` や `BeforeEach` のようなブロックではなく、スクリプトの最上部で実行します。
2.  **フルパスを直接指定:** `-Name` パラメータではなく、`.psd1` マニフェストファイルへの**解決済みのフルパス**を直接渡します。
3.  **`-Force` オプション:** 実行ごとにモジュールが再ロードされることを保証します。
4.  **`-ErrorAction Stop` と `try-catch`:** インポート自体が失敗した場合、テストを続行せずに即座に停止させ、問題に気づけるようにします。

```powershell
# 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 -Be` を `Should Be` に、`Should -BeNullOrEmpty` を `Should BeNullOrEmpty` に、機械的に一括変換しました。** これで構文エラーは完全に撲滅できました。

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

*   **VS CodeのPowerShellバージョン:** 最終的に、VS Codeが使用するPowerShellを、システム標準の `Windows PowerShell (5.1)` から、別途インストールした `PowerShell 7.x` に `settings.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` の設定例などを掲載)*