tag:crieit.net,2005:https://crieit.net/tags/Pester/feed 「Pester」の記事 - Crieit Crieitでタグ「Pester」に投稿された最近の記事 2023-10-03T23:51:18+09:00 https://crieit.net/tags/Pester/feed tag:crieit.net,2005:PublicArticle/18588 2023-10-03T09:07:43+09:00 2023-10-03T23:51:18+09:00 https://crieit.net/posts/Pester-v3-v5-PowerShell Pester v3 & v5 互換のテストコードを書く (PowerShell) <p>PowerShell のテストフレームワークである Pester は、単体テストや自動テストなどを手軽に書けるので、スクリプトやコードの品質を保つのに役立つ。</p> <p>しかし、 Pester は v3 から v5 にかけてかなり大きな破壊的変更がある。</p> <p>この記事では、 Pester v3 と v5 の両方に互換があるテストコードの書き方について紹介したい。</p> <h2 id="何故 v3 と v5 で互換を取りたいか"><a href="#%E4%BD%95%E6%95%85+v3+%E3%81%A8+v5+%E3%81%A7%E4%BA%92%E6%8F%9B%E3%82%92%E5%8F%96%E3%82%8A%E3%81%9F%E3%81%84%E3%81%8B">何故 v3 と v5 で互換を取りたいか</a></h2> <p>Windows 10 や 11 では、デフォルトで Windows PowerShell 5.1 がインストールされており、更にそこには Pester モジュールの v3.4.0 もインストールされている。<br /> Windows 上の PowerShell モジュールディレクトリはシステム全体で共通のため、たとえ別途 PowerShell 7.3 LTS などをインストールしていたとしても、既定では Pester v3.4.0 が読み込まれるわけだ。</p> <p>一方、 Linux 等のそれ以外のシステムで PowerShell をインストールした場合は、 Pester が自動的にインストールされることはないため、 Pester モジュールを追加でインストールすることになる。<br /> このとき、普通は最新版の v5 が入るだろう。</p> <p>Windows PowerShell の Pester を v5 に更新させたり、 Linux 等のシステムでインストールする Pester モジュールを v3.4 に抑えさせたりできればよいが、まぁなかなかそうも行かない時がある。</p> <p>そんなのっぴきならん状況だと、多少苦労してでもテストコード側を Pester v3 と v5 どちらにも互換を取ってしまったほうが手っ取り早いかも…なんて状況が地球上の何処かには存在するかもしれない。</p> <h2 id="テストコードの書き方"><a href="#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9">テストコードの書き方</a></h2> <p>v3 と v4 の間の破壊的変更で一番デカいのは、 <code>Should</code> 構文の変更で互換切りを行っている部分だ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/breaking-changes-in-v5">https://pester.dev/docs/migrations/breaking-changes-in-v5</a></p> <blockquote> <ul> <li>Legacy syntax <code>Should Be</code> (without <code>-</code>) is removed, see <a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/v3-to-v4">Migrating from Pester v3 to v4</a></li> </ul> </blockquote> <p>構文自体の変更自体は v4 で行われたものだが、このときは以前の構文も互換性が残されていた。<br /> ところが、 v5 で早速この互換が切られてしまっている。</p> <p>コレのせいで、ほぼすべてのテストコードが、そのままでは v3 と v5 の間で互換を取ることが不可能になっている。<br /> 何考えてんだ……</p> <p>位置指定パラメータからスイッチパラメータに変わっているため、このどちらにも渡せるようにするためには <a target="_blank" rel="nofollow noopener" href="https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_splatting">Splatting 表記</a> に頼ることにする。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/pester/Pester/wiki/Should-v3">v3 で使える <code>Should</code></a> のオペレーターそれぞれについて、 v3 以下なら位置指定パラメータで渡す方法で、 v4 以上ならスイッチパラメータで渡す方法で、それぞれ <code>$BeForCompat</code> のような形で <code>$*ForCompat</code>, <code>$Not*ForCompat</code> の形式の変数をテストコード開始時に定義する。<br /> そして、その変数を <code>Should</code> で <code>@BeForCompat</code> のように Splatting 表記にて使用する。</p> <p>また、 v4 では <code>Contain</code>/<code>ContainExactly</code> オペレーターが <a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/v3-to-v4#update-to-new-names"><code>-FileContentMatch</code>/<code>-FileContentMatchExactly</code></a> にリネームされていたり、<br /> v5 では <code>*.Tests.ps1</code> <a target="_blank" rel="nofollow noopener" href="https://pester.dev/docs/migrations/v4-to-v5#put-setup-in-beforeall">ファイル先頭や、 <code>Context</code>, <code>Describe</code> 直下にセットアップコードが記載できなくなっている</a>ので、 全て <code>BeforeAll</code> ブロック内に記述する必要がある。</p> <p>これらのマイグレーションをまとめると、 v3/v5 互換の Pester テストコードは以下のようになる。<br /> (<code>v3-to-v5-Migrations.Tests.ps1</code> というファイル名で保存されているものとする)</p> <pre><code class="powershell">#Requires -Version 5 #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="3.0" } trap { break; } $GlobalBeforeAll = { # Write here the code that needs to be executed at the start of the test $shouldParams = 'Be','BeExactly','BeGreaterThan','BeLessThan','BeLike','BeLikeExactly','BeNullOrEmpty','BeOfType','Exist','Match','MatchExactly','Throw'; if ((Get-Module Pester).Version -ge [version]'4.0') { $shouldParams | ForEach-Object { Set-Variable "${_}ForCompat" @{ $_ = $true }; Set-Variable "Not${_}ForCompat" @{ Not = $true; $_ = $true }; }; $FileContentMatchForCompat = @{ FileContentMatch = $true }; $FileContentMatchExactlyForCompat = @{ FileContentMatchExactly = $true }; $NotFileContentMatchForCompat = @{ Not = $true; FileContentMatch = $true }; $NotFileContentMatchExactlyForCompat = @{ Not = $true; FileContentMatchExactly = $true }; } else { $shouldParams | ForEach-Object { Set-Variable "${_}ForCompat" @($_); Set-Variable "Not${_}ForCompat" @('Not', $_); }; $FileContentMatchForCompat = @('Contain'); $FileContentMatchExactlyForCompat = @('ContainExactly'); $NotFileContentMatchForCompat = @('Not','Contain'); $NotFileContentMatchExactlyForCompat = @('Not','ContainExactly'); } } if ((Get-Module Pester).Version -ge [version]'5.0') { BeforeAll $GlobalBeforeAll; } else { . $GlobalBeforeAll; } Describe 'Pester v3, v5 compatibility notation test' { BeforeAll { # DON'T use $MyInvocation.MyCommand.Path $filePath = '.\v3-to-v5-Migrations.Tests.ps1'; } It "Pattern: (<A>, <B>)" -TestCases @( @{A=1; B=2}; @{A=3; B=4}; ) { param ($A, $B); ($A / $A) | Should @BeForCompat 1; ($B * $A) | Should @NotBeForCompat 0; $filePath | Should @FileContentMatchExactlyForCompat 'BeforeAll'; } } </code></pre> <pre><code class="console">PS > # Pester 3.4.0 の場合 PS > Get-Module Pester ModuleType Version PreRelease Name ---------- ------- ---------- ---- Script 3.4.0 Pester PS > Invoke-Pester Describing Pester v3, v5 compatibility notation test [+] Pattern: (1, 2) 74ms [+] Pattern: (3, 4) 5ms Tests completed in 53ms Passed: 2 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0 PS > </code></pre> <pre><code class="console">PS > # Pester 5.5.0 の場合 PS /> Get-Module Pester ModuleType Version PreRelease Name ---------- ------- ---------- ---- Script 5.5.0 Pester PS > Invoke-Pester Starting discovery in 1 files. Discovery found 2 tests in 21ms. Running tests. [+] .../v3-to-v5-Migrations.Tests.ps1 115ms (82ms|13ms) Tests completed in 117ms Tests Passed: 2, Failed: 0, Skipped: 0 NotRun: 0 PS > </code></pre> <p>うーん、面倒くさいね。</p> <h2 id="Tips"><a href="#Tips">Tips</a></h2> <p>こんなアホなことはせずに、Pester v3 なら v3 のコードで、 v5 なら v5 のコードで記述する場合、せめてテストコードの先頭で <a target="_blank" rel="nofollow noopener" href="https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_requires"><code>#Requires</code> 構文</a> を書いて、どのバージョンの Pester をターゲットにしているかしっかり明示しよう。</p> <pre><code class="powershell"># Pester v3, v4 をターゲットにする場合 #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="3.0"; MaximumVersion="4.99" } (Get-Module Pester).Version; Describe 'desc' { It 'it' { 1 | Should Be 1; }; }; </code></pre> <pre><code class="powershell"># Pester v5 以降をターゲットにする場合 #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="5.0" } BeforeAll { (Get-Module Pester).Version; }; Describe 'desc' { It 'it' { 1 | Should -Be 1; }; }; </code></pre> <p>本来 <code>#Requires</code> 構文には、必要なモジュールが現在のセッションにない場合自動でインポートする機能があり、例えば複数のバージョンのモジュールがサイドバイサイドでインストールされていた場合だと、条件に一致するバージョンのものがインポートされる便利な機能がある。<br /> しかし Pester の場合は、必ずしも良い感じには機能してくれるとは限らない。</p> <p>テストコードが読み込まれる前 <code>Invoke-Pester</code> した時点で Pester モジュールのインポートが走ってしまうことや、複数のバージョンのモジュールがインポートされた状態になると、いずれか一つのモジュールが条件を満たせば <code>#Requires</code> の検証をパスしてしまうのに、インポートされたもののうち最も古いバージョンのモジュールで実行されてしまうような挙動をとるためだ。</p> <p>それでも、 Pester モジュールがひとつしかインストールされていないような多くの状況では、バージョンを明示しておくことでエラー原因がわかりやすくはなるはずだ。</p> advanceboy