PowerShellでMarkdownパーサー自作してPester地獄から生還した話 ～VS Codeの設定も見直そう～

## はじめに: MarkdownでWBSを管理したい

こんにちは！普段PowerShellで業務効率化スクリプトを書いているのですが、もっと複雑なWBS（Work Breakdown Structure）をMarkdownで手軽に管理し、ゆくゆくはExcelとも連携できるツールを自作しようと思い立ちました。名付けて「MD-WBS-Tools」プロジェクトです。

Markdownのシンプルさとテキストベースの管理のしやすさは魅力ですが、これをPowerShellでパースし、さらにPesterで単体テストを書こうとしたところ、予想外の「地獄」が待っていました…。

この記事では、その過程で遭遇した数々のエラーと、その解決に至るまでの試行錯誤、そして意外なところに潜んでいた根本原因について共有したいと思います。PowerShellで自作ツール開発やPesterテストに挑戦している方の、何かしらの参考になれば幸いです。

## やりたかったこと: `simple-md-wbs` 記法とツール概要

私が考えた `simple-md-wbs` は、Markdownの見出しとリストを使い、属性情報をコメントや専用行に記述するシンプルなものです。

```markdown
# プロジェクトX

## D1_大分類A

%% ID01,2025-01-01,2025-01-10,5,先行,PREV01,2025-01-02,2025-01-09,50%,担当A,組織X,2025-01-08,コメント

- タスクA <!-- T01,2025-01-01,,,,,,,,,,コメント -->
```

これをPowerShellでパースし、CSVに出力したり、将来的にはCSVからこの形式に戻したりするツールを作るのが目標です。そして、そのコアとなるパース処理の関数をPesterでテストしようとしました。

## Pester地獄の始まり: エラーまたエラー

意気揚々とテストを書き始めたものの、実行するたびに赤いエラーメッセージがコンソールを埋め尽くしました。

1. **`CommandNotFoundException` の嵐:**
    * モジュールに関数を定義し、テストスクリプトで `Import-Module` しているはずなのに、`The term '関数名' is not recognized...` と怒られる日々。
    * **原因と対策:**
        * モジュールマニフェスト (`.psd1`) の `FunctionsToExport` や、モジュールスクリプト (`.psm1`) の `Export-ModuleMember` で、テストしたい関数が正しくエクスポートされていませんでした。灯台下暗し！
        * 関数名をリファクタリングした際に、エクスポート設定を更新し忘れるという初歩的なミスも…。

2. **Pester構文の罠: `-Be` vs `Be`**
    * `RuntimeException: Legacy Should syntax (without dashes) is not supported in Pester 5.`
    * `ParameterBindingValidationException: コマンドレットの動的パラメーターを取得できません。`
    * **原因と対策:**
        * 以前Pester 3.x系を使っていた名残で、アサーションを `Should -Be` のようにハイフン付きで書いていました。Pester 5.xではハイフンなしの `Should Be` が正しい構文です。
        * 最初は一部だけ直して動いたように見えたテストも、他の箇所に古い構文が残っていると、Pesterがテスト構造を正しく解釈できず、`ParameterBindingValidationException` のような間接的なエラーを引き起こすことが判明。**ファイル全体を検索して徹底的に置換**することで解決しました。

3. **状態を持つ関数のテストと `BeforeEach` の挙動:**
    * WBSのIDを階層的に自動生成する関数 (`Get-DecodedAndMappedAttribute`) は、内部でカウンターを保持しています。テストごとにカウンターをリセットするため `BeforeEach { Reset-WbsCounters }` を使いました。
    * しかし、これだけでは不十分です。各テストケース（`It` ブロック）が独立して正しい結果を出す必要があります。そのテストが必要とする前提条件（上位階層のIDがすでに生成されているなど）を、各テストケース内で再現する必要がありました。
    * **教訓:** ステートフルな関数のテストは、状態管理とテストの独立性をどう担保するかが鍵。`BeforeEach` での状態リセットと、テストケース内での前提条件セットアップの組み合わせが重要。

4. **謎の `ParameterBindingValidationException` とエンコーディング:**
    * Pester構文を修正しても、`Get-DecodedAndMappedAttribute Tests` の `BeforeEach` で `Reset-WbsCounters` を呼ぶ箇所で `ParameterBindingValidationException: 引数が null であるため、パラメーター 'Name' にバインドできません。` が出続けました。
    * **原因の切り分け:** `Reset-WbsCounters` 関数自体に `-Name` `パラメータ`はなく、呼び出し方も間違っていない。Pesterがテストブロックを解釈する内部処理で問題が起きている可能性を疑いました。
    * **対策 (効果あり):** テストスクリプトファイル (`MyCommonFunctions.Test.ps1`) のエンコーディングを **UTF-8 with BOM** で保存し直したところ、このエラーが解消！ 日本語環境におけるPester 5.xとBOMなしUTF-8の相性問題だった可能性があります。

## 灯台下暗し: VS CodeのPowerShell実行バージョン

上記のエラーと格闘する中で、もう1つ大きな見落としがありました。
それは、**VS CodeがターミナルやPowerShell拡張機能でどのバージョンのPowerShellを使用しているか**という設定です。

私の環境では、PowerShell 7.xを別途インストールしていましたが、VS CodeのデフォルトはWindows PowerShell(5.1) のままでした。
Pester 5.xはPowerShell Core（6以降）での利用が推奨されており、PowerShell 5.1との組み合わせでは不安定な挙動を示すことがあります。

**`settings.json` の修正:**
VS Codeの `settings.json` に以下を追記（または修正）し、デフォルトで使用するPowerShellのバージョンを明示的にPowerShell 7.xにしました。

```json
{
    // ... 他の設定 ...
    "powershell.powerShellAdditionalExePaths": {
      "PowerShell 7.5.1": "C:\\Program Files\\PowerShell\\7\\pwsh.exe" // ← ご自身のインストールパスに合わせて
    },
    "powershell.powerShellDefaultVersion": "PowerShell 7.5.1", // ← 追加または修正
    // ... 他の設定 ...
}
```

この設定変更により、Pesterの動作が格段に安定し、原因不明のエラーが減少したように感じます。**これまでの多くの問題の根底に、この実行バージョンの不一致があった可能性も否定できません。**

### 属性パース関数のデバッグと期待値の調整

`ConvertTo-AttributeObject` 関数のテストでは、多くの属性が省略されたり、コメントにカンマや特殊文字が含まれたりするケースで、期待値と実際の出力がなかなか一致しませんでした。

これは、関数のパースロジック（とくに省略されたフィールドの扱いや、コメントの範囲特定）と、テストケースで記述する期待値の間の微妙なズレが原因でした。
関数のデバッグ出力（`Write-Host` でパース途中の情報を表示）とテストの実行結果を何度も見比べ、**関数の実際の挙動に合わせてテストケースの期待値を正確に記述し直す**ことで、最終的にすべてのテストをパスさせることができました。

**教訓:** テストは、書くだけでなく、対象関数のロジックを深く理解し、期待値を正確に設定することが重要。デバッグ出力は友達。

### 現在の成果と今後の展望

苦労の末、コアとなるモジュール関数 (`Get-DecodedAndMappedAttribute`, `ConvertTo-AttributeObject`, `Reset-WbsCounters`) の単体テストはすべてパスするようになりました！

```powershell
# MyCommonFunctions.Test.ps1 の最終的な実行結果（イメージ）
Tests completed in xxxms
Tests Passed: 13, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
```

現在は、このモジュールを利用するメインスクリプト (`Convert-SimpleMdWbsToCsv.ps1`) のリファクタリングと、依存関係解決機能の実装を進めているところです。
将来的には、CSVから `simple-md-wbs` への逆変換や、Excel VBAマクロとの連携による表示最適化も視野に入れています。

### まとめ: PowerShellとPesterと格闘して得た教訓

* **Pesterのバージョンと構文は正確に合わせよう。**（とくにV3/V4からV5への移行時）
* **モジュールの関数は正しくエクスポートしよう。**（マニフェストファイル大事）
* **状態を持つ関数のテストは、状態管理と独立性を意識しよう。**（`BeforeEach` とリセット関数）
* **ファイルのエンコーディングは「UTF-8 with BOM」が無難（とくに日本語環境）。**
* **VS CodeのPowerShell実行バージョン設定を見直そう！**（これが一番の盲点だったかも）
* **テストの期待値は、関数の実際の挙動と寸分違わず合わせよう。**（デバッグ出力は神）

PowerShellでのツール開発は非常に強力ですが、テスト環境を整え、細部まで注意を払うことの重要性を改めて痛感しました。
この記事が、同じようにPowerShellやPesterで奮闘している誰かの一助となれば幸いです。

---
**（付録）主要なスクリプトや設定ファイルのスニペット**
*（ここには、記事本文で触れた `MyCommonFunctions.psm1` の関数定義の一部、`MyCommonFunctions.Test.ps1` の代表的なテストケース、`settings.json` の設定例などを掲載すると良いでしょう）*