From 343c0ef8f890f39757916ab487589d46aa5c79b0 Mon Sep 17 00:00:00 2001 From: fadwen <110697945+fadwen@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:38:56 +0000 Subject: [PATCH] chore: sync Copilot instructions from standards repo --- .github/copilot-instructions.md | 9 +- .github/instructions/cicd.instructions.md | 2 +- .github/instructions/module.instructions.md | 9 +- .../pester-supporting-docs/assertion-guide.md | 47 +++- .../cicd-integration.md | 81 ++++--- .../custom-assertions.md | 12 +- .../integration-test-template.md | 4 +- .../mocking-patterns.md | 21 +- .../performance-test-template.md | 9 +- .../pester-configuration.md | 165 ++++++++++--- .../security-test-template.md | 4 +- .../pester-supporting-docs/test-data-guide.md | 9 +- .../pester-supporting-docs/test-execution.md | 67 +++--- .../test-structure-guide.md | 69 ++++-- .../unit-test-template.md | 15 +- .../pester-supporting-docs/v6-migration.md | 220 +++++++++++++++--- .github/instructions/pester.instructions.md | 45 ++-- .github/prompts/create-test.prompt.md | 4 +- 18 files changed, 603 insertions(+), 189 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 28cc7e2..3402493 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -540,18 +540,17 @@ try { ### Pester Test Template -Targets Pester 6.1+. See [Testing Framework](./instructions/pester.instructions.md) for the +Targets Pester 6.2+. See [Testing Framework](./instructions/pester.instructions.md) for the full standards and [Assertion Guide](./instructions/pester-supporting-docs/assertion-guide.md) for the `Should-*` reference. ```powershell -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } Describe "Get-ExampleData" -Tag "Unit" { BeforeAll { # Pester 6 discovers and runs one file at a time, so each file - # imports its own dependencies. Only one BeforeAll per block - - # duplicates throw. + # imports its own dependencies. Import-Module $PSScriptRoot\..\ModuleName.psd1 -Force # Mock external dependencies following community patterns @@ -688,7 +687,7 @@ Use these prompts for quality assurance: - **Community Standards**: Reference `.github/instructions/community-standards.instructions.md` - **Style Guide**: Reference `.github/instructions/style-enforcement.instructions.md` - **Troubleshooting**: Always organized in `./Troubleshooting/` folder structure -- **Testing**: Use Pester 6.1+ with comprehensive coverage requirements +- **Testing**: Use Pester 6.2+ with comprehensive coverage requirements - **Help Docs**: Use Microsoft.PowerShell.PlatyPS 1.0.3+ — Markdown in `docs/`, MAML in `en-US/` - **Security**: Implement defense-in-depth with community-approved patterns - **Performance**: Optimize using community-identified best practices and expert feedback diff --git a/.github/instructions/cicd.instructions.md b/.github/instructions/cicd.instructions.md index 8cb37bc..78b0783 100644 --- a/.github/instructions/cicd.instructions.md +++ b/.github/instructions/cicd.instructions.md @@ -114,7 +114,7 @@ not the audit configuration. ```powershell # Pester 6 test execution with coverage -Import-Module Pester -MinimumVersion 6.1.0 -Force +Import-Module Pester -MinimumVersion 6.2.0 -Force $pesterConfig = New-PesterConfiguration $pesterConfig.Run.Path = './Tests' diff --git a/.github/instructions/module.instructions.md b/.github/instructions/module.instructions.md index 6f68d52..7e976d0 100644 --- a/.github/instructions/module.instructions.md +++ b/.github/instructions/module.instructions.md @@ -381,11 +381,11 @@ function Get-ModuleCredential { ### Pester Test Structure -Generate comprehensive test suites targeting **Pester 6.1+**: +Generate comprehensive test suites targeting **Pester 6.2+**: ```powershell # Tests/Unit/Public/Verb-Noun.Tests.ps1 -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } BeforeAll { # Pester 6 discovers and runs one file at a time - each file must be self-contained @@ -411,7 +411,6 @@ Describe "Verb-Noun" -Tag "Unit", "Public" { } Context "Functionality" { - # Only one BeforeEach per block - Pester 6 throws on duplicates BeforeEach { Mock External-Dependency { "MockedResult" } } @@ -437,8 +436,8 @@ Describe "Verb-Noun" -Tag "Unit", "Public" { ``` Key Pester 6 points: `Should-*` assertions replace `Should -Be` for new tests, -`Assert-MockCalled` was removed in favour of `Should-Invoke`, duplicate setup blocks throw, and -every file must import its own dependencies. See +`Assert-MockCalled` was removed in favour of `Should-Invoke`, and every file must import its own +dependencies. See [Pester instructions](./pester.instructions.md) and the [migration guide](./pester-supporting-docs/v6-migration.md). diff --git a/.github/instructions/pester-supporting-docs/assertion-guide.md b/.github/instructions/pester-supporting-docs/assertion-guide.md index 16bc647..e25e92c 100644 --- a/.github/instructions/pester-supporting-docs/assertion-guide.md +++ b/.github/instructions/pester-supporting-docs/assertion-guide.md @@ -1,6 +1,6 @@ # Pester 6 Assertion Guide -Targets **Pester 6.1+**. +Targets **Pester 6.2+**. Pester 6 ships a new family of `Should-*` assertions (dash, no space) alongside the classic `Should -Be` operator. Both work. This guide covers which to use and how they differ. @@ -25,8 +25,9 @@ The classic `Should` routes `-Be`, `-BeExactly`, `-Contain` and friends through left side is always unwrapped by the pipeline and failure messages have to guess intent. The `Should-*` assertions are specialized and type-aware: -- Failure messages are precise (string diffs point a caret at the first differing character; - collection comparisons point at the first differing index). +- Failure messages are precise (a short string diff points a caret at the first differing character + and a long one prints every differing region with line numbers; collection comparisons point at + the first differing index). - `$Expected` drives the comparison type, so `1 | Should-Be $true` compares as booleans. - Type-specific switches live where they belong (`Should-BeString -IgnoreWhitespace`). - `$null`, empty collections, and single-item arrays behave consistently. @@ -138,6 +139,34 @@ Get-Content $path -Raw | Should-BeString $expected -NormalizeLineEnding `Should-NotBeString` takes both `-TrimWhitespace` and `-NormalizeLineEnding`, and its `-Expected` is mandatory. +#### Long strings get a real diff + +From 6.2, when either string is longer than 10 lines or 120 characters, a failing `Should-BeString` +prints every region that differs with context, instead of a caret under the first difference. +Expected line numbers are on the left and actual on the right, so the output stays readable when +lines were added or removed and the two sides stop lining up: + +```text +Expected strings to be the same, but they were different. +Expected length: 860 +Actual length: 834 +Expected 30 line(s), actual 30 line(s). +2 regions differ. + 3 3 | line 3 of the expected text + 4 4 | line 4 of the expected text + 5 | - line 5 of the expected text + 5 | + line 5 CHANGED + 6 6 | line 6 of the expected text + ... + 17 | - line 17 of the expected text + 17 | + line 17 CHANGED +``` + +Only the differing lines are expanded, so a tab or trailing space shows up without the unchanged +context turning into escape codes. This is the assertion to use for snapshot tests, rendered +templates, and generated configuration files - pair it with `-NormalizeLineEnding`. Short strings +keep the caret. + ### Booleans and null ```powershell @@ -267,6 +296,18 @@ $record.Created | Should-BeBefore ([datetime]::Now) the TimeSpan-vs-double comparison mistakes that pattern invites. Its `-Expected` is mandatory, as is `Should-BeSlowerThan`'s - a bare `Should-BeFasterThan` no longer binds to a silent default. +Both take a `[scriptblock]` to measure or a `[timespan]` to compare, and nothing else. From 6.2 +anything else **throws**: + +```powershell +{ Invoke-Thing } | Should-BeFasterThan 1s # measures the scriptblock +(Invoke-Thing) | Should-BeFasterThan 1s # 6.1: passed without measuring anything. 6.2: throws +# Expected a [scriptblock] to measure or a [timespan] to compare, but got [string] 'result'. +``` + +On 6.1 a string, a number, or `$null` fell through the assertion and the test passed having asserted +nothing. A performance test that starts failing on 6.2 with that message was never measuring. + ### Deep object comparison `Should-BeEquivalent` walks nested properties, hashtables, dictionaries, and collections and emits diff --git a/.github/instructions/pester-supporting-docs/cicd-integration.md b/.github/instructions/pester-supporting-docs/cicd-integration.md index b32e686..50a8e20 100644 --- a/.github/instructions/pester-supporting-docs/cicd-integration.md +++ b/.github/instructions/pester-supporting-docs/cicd-integration.md @@ -1,6 +1,6 @@ # CI/CD Integration Guide -Targets **Pester 6.1+**. Pester 6 supports **Windows PowerShell 5.1** and **PowerShell 7.4+** only - +Targets **Pester 6.2+**. Pester 6 supports **Windows PowerShell 5.1** and **PowerShell 7.4+** only - support for PowerShell 3, 4, 6, and early/unsupported 7.x was removed, so drop `7.2` and `7.3` from existing test matrices. @@ -14,7 +14,7 @@ text descriptions and standard ASCII characters only. ## Job Design for Pester 6 -Two facts shape the pipeline: +Three facts shape the pipeline: 1. **Coverage and parallel pull against each other.** Pester 6.0 refused to combine them at all; 6.1 merges coverage across workers but forces slower breakpoint-based collection to do it. Either @@ -23,13 +23,18 @@ Two facts shape the pipeline: 2. **Discovery failures do not appear in `FailedCount`.** A file that fails discovery contributes zero failed tests. Every gate must also check `FailedContainersCount`, and a cheap discovery-only job should run first. +3. **Pester 6.0 and 6.1 do not import on PowerShell 7.4.0 to 7.4.5.** Their `net8.0` assembly + referenced a newer `System.Management.Automation` than those releases carry, so `Import-Module` + failed with `Cannot convert "PesterConfigurationDeserializer" from String to Type`. 6.2 fixed it. + Hosted runners ship the latest 7.4.x, so this bites self-hosted agents and pinned container + images - pin Pester at 6.2.0 or newer wherever a 7.4 leg remains. ```text validate (discovery-only, fast) | - +--> test-parallel (no coverage, PS 7.4+, fast feedback) + +--> test-parallel (no coverage, fast feedback) +--> test-coverage (sequential, coverage gate) - +--> test-ps51 (Windows PowerShell 5.1, sequential - no parallel support) + +--> test-ps51 (Windows PowerShell 5.1; parallel works here too from 6.2) ``` ## GitHub Actions Integration @@ -49,12 +54,12 @@ on: env: POWERSHELL_TELEMETRY_OPTOUT: 1 - PESTER_VERSION: '6.1.0' + PESTER_VERSION: '6.2.0' jobs: - # Fast structural check. Catches the Pester 6 breakages - duplicate setup blocks, - # empty -ForEach, files that cannot be discovered independently - without running - # a single test. + # Fast structural check. Catches the Pester 6 breakages - empty -ForEach, files + # that cannot be discovered independently, a wrong-typed configuration value - + # without running a single test. validate: runs-on: ubuntu-latest steps: @@ -119,7 +124,7 @@ jobs: os: [windows-latest, ubuntu-latest, macos-latest] shell: [pwsh] include: - # Windows PowerShell 5.1 - sequential only, no parallel support + # Windows PowerShell 5.1 - Run.Parallel works here from Pester 6.2 - os: windows-latest shell: powershell @@ -160,17 +165,17 @@ jobs: throw "PSScriptAnalyzer found $($analysisResults.Count) issues" } - # Fast feedback: parallel, no coverage. Parallel is silently ignored on - # Windows PowerShell 5.1, which falls back to sequential with a warning. + # Fast feedback: parallel, no coverage. From Pester 6.2 the parallel runner + # works on Windows PowerShell 5.1 too; 6.0 and 6.1 fell back to sequential there. - name: Run Unit Tests shell: ${{ matrix.shell }} run: | - Import-Module Pester -MinimumVersion 6.1.0 -Force + Import-Module Pester -MinimumVersion 6.2.0 -Force $config = New-PesterConfiguration $config.Run.Path = './Tests/Unit' $config.Run.PassThru = $true - $config.Run.Parallel = $PSVersionTable.PSVersion.Major -ge 7 + $config.Run.Parallel = $true $config.TestResult.Enabled = $true $config.TestResult.OutputFormat = 'NUnitXml' $config.TestResult.OutputPath = './TestResults.xml' @@ -187,7 +192,7 @@ jobs: - name: Run Integration Tests shell: ${{ matrix.shell }} run: | - Import-Module Pester -MinimumVersion 6.1.0 -Force + Import-Module Pester -MinimumVersion 6.2.0 -Force $config = New-PesterConfiguration $config.Run.Path = './Tests/Integration' @@ -233,7 +238,7 @@ jobs: - name: Run Tests with Coverage shell: pwsh run: | - Import-Module Pester -MinimumVersion 6.1.0 -Force + Import-Module Pester -MinimumVersion 6.2.0 -Force $config = New-PesterConfiguration $config.Run.Path = './Tests/Unit' @@ -308,7 +313,7 @@ jobs: - name: Run Security Tests shell: pwsh run: | - Import-Module Pester -MinimumVersion 6.1.0 -Force + Import-Module Pester -MinimumVersion 6.2.0 -Force $config = New-PesterConfiguration $config.Run.Path = './Tests/Security' @@ -372,7 +377,7 @@ jobs: - name: Run Performance Tests shell: pwsh run: | - Import-Module Pester -MinimumVersion 6.1.0 -Force + Import-Module Pester -MinimumVersion 6.2.0 -Force $config = New-PesterConfiguration $config.Run.Path = './Tests/Performance' @@ -485,7 +490,7 @@ stages: targetType: 'inline' script: | Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module Pester -MinimumVersion 6.1.0 -Force -Scope CurrentUser + Install-Module Pester -MinimumVersion 6.2.0 -Force -Scope CurrentUser Install-Module PSScriptAnalyzer -Force -Scope CurrentUser pwsh: $(powershellVersion -eq '7.x') @@ -507,7 +512,7 @@ stages: inputs: targetType: 'inline' script: | - Import-Module Pester -MinimumVersion 6.1.0 -Force + Import-Module Pester -MinimumVersion 6.2.0 -Force $config = New-PesterConfiguration $config.Run.Path = './Tests' @@ -575,7 +580,7 @@ stages: targetType: 'inline' script: | Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module Pester -MinimumVersion 6.1.0 -Force -Scope CurrentUser + Install-Module Pester -MinimumVersion 6.2.0 -Force -Scope CurrentUser pwsh: true - task: PowerShell@2 @@ -670,7 +675,7 @@ pipeline { steps { powershell ''' Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module Pester -MinimumVersion 6.1.0 -Force -Scope CurrentUser + Install-Module Pester -MinimumVersion 6.2.0 -Force -Scope CurrentUser $config = New-PesterConfiguration $config.Run.Path = './Tests' @@ -697,7 +702,7 @@ pipeline { steps { pwsh ''' Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module Pester -MinimumVersion 6.1.0 -Force -Scope CurrentUser + Install-Module Pester -MinimumVersion 6.2.0 -Force -Scope CurrentUser ./Invoke-Tests.ps1 -TestType All -Environment CI -CodeCoverage ''' @@ -775,7 +780,7 @@ variables: .powershell_template: &powershell_template before_script: - Set-PSRepository PSGallery -InstallationPolicy Trusted - - Install-Module Pester -MinimumVersion 6.1.0 -Force -Scope CurrentUser + - Install-Module Pester -MinimumVersion 6.2.0 -Force -Scope CurrentUser # GitLab needs JUnit for test results and Cobertura for coverage. Pester 6 # supports both natively - set TestResult.OutputFormat = 'JUnitXml' and @@ -849,7 +854,7 @@ Pester 6 has a built-in parallel runner. `Run.Container` is for _parametrizing_ parallelism - the old snippet using multiple containers ran sequentially. ```powershell -# Actual parallel execution: one file per runspace, PowerShell 7+ only +# Actual parallel execution: one file per runspace, on 5.1 and 7 alike from 6.2 $config = New-PesterConfiguration $config.Run.Path = './Tests/Unit' $config.Run.Parallel = $true @@ -871,22 +876,30 @@ jobs is still the better default - a parallel job without coverage for feedback, job with the profiler for the gate - but measure before assuming either is faster. Each worker starts from a **clean runspace**, so every test file must be self-contained. Provide -shared bootstrap through a `Pester.BeforeContainer.ps1` at the repository root, which Pester -dot-sources before every container: +shared bootstrap through `Pester.BeforeContainer.ps1` files, which Pester dot-sources before every +container - from 6.2, every one from `Run.RepoRoot` down to the test file's folder, outermost first. +Put run-time setup in a `BeforeAll`; top-level code in the file runs at discovery only: ```powershell # Pester.BeforeContainer.ps1, at the repository root -. "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +BeforeAll { + . "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +} ``` The `Run.BeforeContainer` option was removed in 6.1 - a CI job that still sets it now throws. In CI -also set `Run.RepoRoot` explicitly, because the default is resolved from the .NET process working -directory and a job that runs Pester from a subdirectory will not find the bootstrap file: +also set `Run.RepoRoot` explicitly. From 6.2 an unset root is resolved from the session's current +location (6.1 used the .NET process working directory), but a checkout without a `.git` directory, +or a run launched from outside the repository, still lands on the wrong root and finds no setup file: ```powershell $config.Run.RepoRoot = $env:GITHUB_WORKSPACE # or the equivalent for your CI system ``` +A parallel worker that died used to drop its file from the results without a trace. From 6.2 the run +throws naming the missing files, and a worker that throws no longer aborts the remaining files when +the job runs with `$ErrorActionPreference = 'Stop'`. + Verify isolation before enabling parallel in CI - if a file only passes as part of a full run, it is not self-contained: @@ -964,3 +977,13 @@ if ($result.CodeCoverage) { Items 2 and 3 are the ones a Pester 5 pipeline will not have, and they are exactly how a v6 upgrade turns green while running fewer tests than before. + +From 6.2 the configuration is part of the gate too. A wrong-typed value - `'true'` as a string in a +`psd1`, say - throws when the configuration is built, and a key that matches no option is reported +once by `Invoke-Pester` as `WARNING: Ignoring configuration keys ...`. A warning does not fail a job, +so check the list before the run: + +```powershell +$unknown = @($config.GetUnknownKeys()) +if ($unknown.Count -gt 0) { throw "Configuration keys that match no option: $($unknown -join ', ')" } +``` diff --git a/.github/instructions/pester-supporting-docs/custom-assertions.md b/.github/instructions/pester-supporting-docs/custom-assertions.md index 154ec95..da08b17 100644 --- a/.github/instructions/pester-supporting-docs/custom-assertions.md +++ b/.github/instructions/pester-supporting-docs/custom-assertions.md @@ -1,6 +1,6 @@ # Custom Assertion Guide -Requires **Pester 6.1+**. `New-ShouldAssertion` does not exist in 6.0.x. +Requires **Pester 6.2+**. `New-ShouldAssertion` does not exist in 6.0.x. **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain text descriptions and standard ASCII characters only. @@ -295,14 +295,18 @@ BeforeAll { ``` For a repository-wide set, import them from a `Pester.BeforeContainer.ps1` at the repository root, -which Pester dot-sources before every container: +which Pester dot-sources before every container. Put the import in a `BeforeAll`: from 6.2 the +file's top-level code runs at discovery only, and while a module import happens to survive into the +run phase (module state is session-wide), a dot-sourced function does not: ```powershell # Pester.BeforeContainer.ps1, at the repository root -Import-Module "$PSScriptRoot/Tests/TestHelpers/Assertions.psd1" -Force +BeforeAll { + Import-Module "$PSScriptRoot/Tests/TestHelpers/Assertions.psd1" -Force +} ``` -That file fires only when `Run.RepoRoot` points at the directory holding it - see +The chain of setup files starts at `Run.RepoRoot` - see [Pester Configuration Guide](./pester-configuration.md). ## Test The Assertion Itself diff --git a/.github/instructions/pester-supporting-docs/integration-test-template.md b/.github/instructions/pester-supporting-docs/integration-test-template.md index 09933b8..69cf641 100644 --- a/.github/instructions/pester-supporting-docs/integration-test-template.md +++ b/.github/instructions/pester-supporting-docs/integration-test-template.md @@ -1,6 +1,6 @@ # Integration Test Template -Targets **Pester 6.1+**. Uses the `Should-*` assertion syntax - see +Targets **Pester 6.2+**. Uses the `Should-*` assertion syntax - see [Assertion Guide](./assertion-guide.md). **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain @@ -11,7 +11,7 @@ text descriptions and standard ASCII characters only. Use this template for integration tests that validate component interactions: ```powershell -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } # Integration tests touch shared external resources - databases, ports, live endpoints - # so they must not run concurrently with other files. diff --git a/.github/instructions/pester-supporting-docs/mocking-patterns.md b/.github/instructions/pester-supporting-docs/mocking-patterns.md index b35ab1f..7c4296a 100644 --- a/.github/instructions/pester-supporting-docs/mocking-patterns.md +++ b/.github/instructions/pester-supporting-docs/mocking-patterns.md @@ -1,6 +1,6 @@ # Mocking Patterns Guide -Targets **Pester 6.1+**. +Targets **Pester 6.2+**. **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain text descriptions and standard ASCII characters only. @@ -823,17 +823,19 @@ Describe "Function Tests" { } ``` -Each `Context` gets its own `BeforeEach`. Pester 6 **throws** on two `BeforeEach` blocks in the -_same_ block, so when you need several groups of mocks, split them into separate `Context` blocks -rather than adding a second setup block. +Each `Context` gets its own `BeforeEach`. From 6.2 a block may also hold several `BeforeEach` +blocks, run in declaration order, so a second group of mocks can be its own block rather than +merged into the first. Separate `Context` blocks remain the better split when the two groups serve +different tests; 6.0 and 6.1 throw on a second `BeforeEach` in the _same_ block. ### Mocks Do Not Cross Files Pester 6 discovers and runs one file at a time, and under `Run.Parallel` each file gets its own runspace. A mock defined in one test file is never visible to another. Define every mock a file needs inside that file. For mock setup shared across many files, dot-source a shared mock factory -from a `Pester.BeforeContainer.ps1` at the repository root - but note that `Mock` itself must still -be called inside a `Describe`/`Context`/`BeforeAll` scope: +from a `Pester.BeforeContainer.ps1` - inside its `BeforeAll`, because from 6.2 top-level code in +that file runs at discovery only and a function it defined would be gone by the time a test calls +it. `Mock` itself must still be called inside a `Describe`/`Context`/`BeforeAll` scope: ```powershell # TestHelpers/MockFactory.ps1 - dot-sourced from Pester.BeforeContainer.ps1 @@ -844,6 +846,13 @@ function Set-StandardExternalMock { } ``` +```powershell +# Pester.BeforeContainer.ps1, at the repository root +BeforeAll { + . "$PSScriptRoot/Tests/TestHelpers/MockFactory.ps1" +} +``` + ```powershell # In each test file BeforeAll { diff --git a/.github/instructions/pester-supporting-docs/performance-test-template.md b/.github/instructions/pester-supporting-docs/performance-test-template.md index b62b741..a7efc2c 100644 --- a/.github/instructions/pester-supporting-docs/performance-test-template.md +++ b/.github/instructions/pester-supporting-docs/performance-test-template.md @@ -1,6 +1,6 @@ # Performance Test Template -Targets **Pester 6.1+**. Uses the `Should-*` assertion syntax - see +Targets **Pester 6.2+**. Uses the `Should-*` assertion syntax - see [Assertion Guide](./assertion-guide.md). **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain @@ -11,7 +11,7 @@ text descriptions and standard ASCII characters only. Use this template for performance benchmarking and regression detection: ```powershell -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } # Performance measurement is meaningless when other test files are competing for the # CPU. This directive keeps the file on the serial path even when Run.Parallel is set. @@ -326,6 +326,11 @@ the actual measured time in the failure message. Prefer it over `Measure-Command for single-shot limits. Keep `Measure-Command` where you need the measurement itself for ratios, averages, or coefficient-of-variation math. +Pipe the **scriptblock**, not its result. From 6.2 both assertions throw when handed anything other +than a `[scriptblock]` or a `[timespan]`; on 6.1 a string, number, or `$null` slipped through and +the test passed without measuring. A limit that starts failing on 6.2 with +`Expected a [scriptblock] to measure or a [timespan] to compare` was never a limit. + ### Compare like with like The most common bug in hand-rolled performance assertions is comparing a `TimeSpan` against a raw diff --git a/.github/instructions/pester-supporting-docs/pester-configuration.md b/.github/instructions/pester-supporting-docs/pester-configuration.md index ef091e5..ef3e86e 100644 --- a/.github/instructions/pester-supporting-docs/pester-configuration.md +++ b/.github/instructions/pester-supporting-docs/pester-configuration.md @@ -1,6 +1,6 @@ # Pester Configuration Guide -Targets **Pester 6.1+**. All settings below were verified against the `PesterConfiguration` object. +Targets **Pester 6.2+**. All settings below were verified against the `PesterConfiguration` object. **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain text descriptions and standard ASCII characters only. @@ -98,8 +98,9 @@ Use this standardized configuration for consistent test execution: ### Settings That Do Not Exist -These appear in older guidance and in a lot of blog posts. They are not real and are silently -ignored when set via a hashtable: +These appear in older guidance and in a lot of blog posts. They are not real. From 6.2 +`Invoke-Pester` warns `Ignoring configuration keys '...', there are no such options` when a hashtable +carries one; before 6.2 they were silently ignored: | Not a setting | Use instead | | --- | --- | @@ -212,8 +213,9 @@ options. You can no longer silently configure a report that never gets written. ## Parallel Execution (Experimental) -Pester 6 can run test **files** concurrently, one file per runspace, using PowerShell 7+ -`ForEach-Object -Parallel`. +Pester 6 can run test **files** concurrently, one file per runspace. From 6.2 the workers run on a +runspace pool, which Windows PowerShell 5.1 and PowerShell 7 both have; 6.0 and 6.1 used +`ForEach-Object -Parallel` and so ran sequentially on 5.1. ```powershell $config = New-PesterConfiguration @@ -223,18 +225,24 @@ $config.Run.ParallelThrottleLimit = 4 # 0 (default) uses all processors Invoke-Pester -Configuration $config ``` -**Requirements**: PowerShell 7+ and file-based containers (`Run.Path`, or `New-PesterContainer --Path` including parametrized files built with `-Data`). +**Requirements**: file-based containers (`Run.Path`, or `New-PesterContainer -Path` including +parametrized files built with `-Data`). **Falls back to a sequential run with a warning** when: -- Running on Windows PowerShell 5.1 - Using in-memory `ScriptBlock` containers - `Run.SkipRemainingOnFailure = 'Run'` +- Running on Windows PowerShell 5.1 with Pester 6.0 or 6.1 If every file opts out with `#pester:no-parallel` the run is simply sequential, with no warning - there is nothing left to parallelize. +**A lost file is an error** from 6.2. A worker that died before returning its result used to vanish +from the run - not failed, not skipped, simply absent, and the run looked green with one file fewer. +The run now compares results to the files it sent and throws naming the missing ones. A worker that +throws is reported with `Write-Error` and the remaining files still report, even under +`$ErrorActionPreference = 'Stop'`, which used to abort the loop at the first failing file. + **Code coverage works under parallel** as of 6.1: each worker measures the same locations and the parent merges the per-location hits into one report. The cost is that coverage in a parallel run is forced onto **breakpoint mode**, because the profiler-based tracer keeps its state in a @@ -264,49 +272,110 @@ change. ### Shared Per-File Setup -Pester dot-sources a **`Pester.BeforeContainer.ps1`** from the repository root before **every** test -file is discovered and run, in both serial and parallel runs. This matters most under parallel, -where each worker starts from a clean runspace: +Pester dot-sources every **`Pester.BeforeContainer.ps1`** it finds between `Run.RepoRoot` and the +test file's own folder before that file is discovered and run, outermost first, in both serial and +parallel runs. This matters most under parallel, where each worker starts from a clean runspace. + +The file follows the same rule as a test file. Top-level code runs during **discovery** only - use it +for what `-ForEach` data or `BeforeDiscovery` needs. Anything the tests need at **run** time goes in +a `BeforeAll`: ```powershell # Pester.BeforeContainer.ps1, at the repository root -Import-Module "$PSScriptRoot/Tests/TestHelpers/Assertions.psd1" -Force -. "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +BeforeAll { + Import-Module "$PSScriptRoot/Tests/TestHelpers/Assertions.psd1" -Force + . "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +} ``` +Before 6.2 the file had no `BeforeAll` and everything sat at top level. That shape silently stops +working on 6.2: a function or variable dot-sourced at top level is gone by the time a test runs +(verified - `CommandNotFoundException` for the helper, `$null` for the variable), and only an +`Import-Module` happens to survive because module state is session-wide. Wrap it. + Because it is a real file it always exposes a stable `$PSScriptRoot` and `$PSCommandPath`, so -relative paths have something reliable to anchor against. +relative paths have something reliable to anchor against. The files run before **every** container, +so what they do must be safe to run more than once. > **Removed in 6.1**: the `Run.BeforeContainer` configuration option. 6.0 shipped both the option > and the convention file; the option had no file to anchor relative paths against, so it was > dropped and the convention file kept. Assigning `$config.Run.BeforeContainer` now throws > `The property 'BeforeContainer' cannot be found on this object`. Move the scriptblock's body into -> `Pester.BeforeContainer.ps1` at the repository root. +> a `BeforeAll` in `Pester.BeforeContainer.ps1` at the repository root. + +### Folder-Scoped Setup (6.2+) + +One file at the root was all 6.1 used. From 6.2 the chain follows the folder structure, so unit and +integration tests can each carry their own setup without repeating it in every test file: + +```text +reporoot/Pester.BeforeContainer.ps1 <- applies to everything +reporoot/Tests/Pester.BeforeContainer.ps1 <- Tests/ and below +reporoot/Tests/Unit/Pester.BeforeContainer.ps1 <- Tests/Unit only +reporoot/Tests/Integration/Pester.BeforeContainer.ps1 <- Tests/Integration only +``` + +```powershell +# reporoot/Tests/Unit/Pester.BeforeContainer.ps1 +BeforeAll { $script:Db = 'in-memory' } + +# reporoot/Tests/Integration/Pester.BeforeContainer.ps1 +BeforeAll { $script:Db = 'real-sql' } +``` + +The files are dot-sourced into the container's own scope, not the run session state, so a file in +`Tests/Integration` never sees what `Tests/Unit` set up, whichever runs first. A folder opts out of +everything above it with a directive, the same meaning `root = true` has in an `.editorconfig`: + +```powershell +# reporoot/Tests/Docs/Pester.BeforeContainer.ps1 +#pester:no-inherit +BeforeAll { Import-Module "$PSScriptRoot/../../Source/ModuleName.psd1" } +``` + +Each container records which setup files applied to it, outermost first, as `BeforeContainerFile` +on the result object - the folder tree cannot show a `#pester:no-inherit`, but this can: + +```powershell +$result = Invoke-Pester -Path ./Tests -PassThru +foreach ($container in $result.Containers) { + $container.Item.Name + $container.BeforeContainerFile | ForEach-Object { " $_" } +} +``` -### Run.RepoRoot Decides Whether It Fires +`Run.SkipRun` applies the chain too, so a discovery-only pass sees the same `-ForEach` data a real +run does. -The convention file is only looked for at `Run.RepoRoot`, and that default is easy to get wrong: +### Run.RepoRoot Decides Where the Chain Starts + +The chain starts at `Run.RepoRoot`, and nothing above it is looked at. Be explicit in any script or +CI job: ```powershell -$config.Run.RepoRoot = $PSScriptRoot # be explicit in any script or CI job +$config.Run.RepoRoot = $PSScriptRoot ``` -`Run.RepoRoot` defaults to the nearest ancestor directory containing `.git`, searched upward from -**`[System.IO.Directory]::GetCurrentDirectory()`** - the .NET process working directory - falling -back to that directory when no `.git` is found. It is resolved once, when `New-PesterConfiguration` -is called. +`Run.RepoRoot` defaults to the nearest ancestor directory containing `.git`, falling back to the +starting directory when no `.git` is found. Where that search starts changed in 6.2: -That is _not_ PowerShell's `$PWD`, and `Set-Location` does not update it: +- `New-PesterConfiguration` still resolves the default from + `[System.IO.Directory]::GetCurrentDirectory()`, the .NET process working directory, which + `Set-Location` does not update. That is the value you see on the object. +- `Invoke-Pester` then re-resolves it from the session's current location (`$PWD`) - **only when + you did not set it**. On 6.1 the run used the process directory, so a session that started + elsewhere and then changed into the repository found no setup file, with nothing to say why. ```powershell Set-Location $repo -(New-PesterConfiguration).Run.RepoRoot.Value # still the directory the process started in +(New-PesterConfiguration).Run.RepoRoot.Value # still the directory the process started in +$result = Invoke-Pester -Configuration $config # 6.2 resolves $repo here; 6.1 used the process directory +$result.Configuration.Run.RepoRoot.Value # what the run actually used ``` -Nor is it derived from `Run.Path`, so pointing Pester at a test directory in another repository does -not move it. When the two diverge the bootstrap silently does not run, and every test that depended -on it fails with `CommandNotFoundException` rather than anything naming the real cause. Set -`Run.RepoRoot` explicitly whenever the run does not start from the repository root. +It is never derived from `Run.Path`, so pointing Pester at a test directory in another repository +does not move it. When the two diverge the bootstrap silently does not run, and every test that +depended on it fails with `CommandNotFoundException` rather than anything naming the real cause. This does **not** replace per-file setup. Each file must still be able to be discovered on its own; see [Pester 6 Migration Guide](./v6-migration.md). @@ -433,15 +502,37 @@ function Merge-HashTable { } ``` -`New-PesterConfiguration -Hashtable` ignores unknown keys silently. Validate what you loaded: +From 6.2 the configuration checks what it is handed. A value of the wrong type throws while the +configuration is built - which bites first with `psd1` and JSON files, where a boolean written in +quotes arrives as a string: + +```powershell +New-PesterConfiguration -Hashtable @{ Run = @{ Parallel = 'yes' } } +# Error: "Run.Parallel expects a bool, but got the string 'yes'." +``` + +An `int` where a `decimal` is expected is still accepted, and a key present with a `$null` value +still means "not set". A key that matches no option is collected rather than thrown on, because a +hashtable may carry keys meant for something else, and `Invoke-Pester` warns about all of them once: + +```text +WARNING: Ignoring configuration keys 'Nonsense', 'Run.Paralel', there are no such options. + Check the spelling, 'Get-Help about_PesterConfiguration' lists all the options. +``` + +A warning is easy to lose in CI output. Read the same list off the object and fail before the run: ```powershell $config = New-PesterConfiguration -Hashtable $mergedConfig -if ($config.CodeCoverage.CoveragePercentTarget.Value -ne $mergedConfig.CodeCoverage.CoveragePercentTarget) { - throw 'Coverage target did not bind - check the setting name' +$unknown = @($config.GetUnknownKeys()) +if ($unknown.Count -gt 0) { + throw "Configuration keys that match no option: $($unknown -join ', ')" } ``` +Before 6.2 both cases were silent - a misspelled key or a wrong-typed value simply did not apply, +and the only defence was reading the value back off the object. + ## Test Filtering Configuration ### Tag-Based Filtering @@ -653,8 +744,8 @@ function Test-PesterConfiguration { # Validate Pester version $pester = Get-Module Pester -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 - if ($pester.Version -lt [version]'6.1.0') { - throw "Pester 6.1+ required, found $($pester.Version)" + if ($pester.Version -lt [version]'6.2.0') { + throw "Pester 6.2+ required, found $($pester.Version)" } # Validate required paths exist @@ -676,9 +767,11 @@ function Test-PesterConfiguration { throw "Invalid coverage target: $target" } - # Parallel is silently ignored on 5.1 - warn rather than let it look enabled - if ($Configuration.Run.Parallel.Value -and $PSVersionTable.PSVersion.Major -lt 7) { - Write-Warning 'Run.Parallel requires PowerShell 7+; the run will fall back to sequential' + # 6.2 collects keys that match no option instead of throwing on them. + # Invoke-Pester warns once; a gate should fail on them instead. + $unknown = @($Configuration.GetUnknownKeys()) + if ($unknown.Count -gt 0) { + throw "Configuration keys that match no option: $($unknown -join ', ')" } } ``` diff --git a/.github/instructions/pester-supporting-docs/security-test-template.md b/.github/instructions/pester-supporting-docs/security-test-template.md index 0e283b1..8e1b4f2 100644 --- a/.github/instructions/pester-supporting-docs/security-test-template.md +++ b/.github/instructions/pester-supporting-docs/security-test-template.md @@ -1,6 +1,6 @@ # Security Test Template -Targets **Pester 6.1+**. Uses the `Should-*` assertion syntax - see +Targets **Pester 6.2+**. Uses the `Should-*` assertion syntax - see [Assertion Guide](./assertion-guide.md). **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain @@ -11,7 +11,7 @@ text descriptions and standard ASCII characters only. Use this template for validating security controls and input sanitization: ```powershell -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } BeforeDiscovery { # Attack-pattern data drives -ForEach, so it MUST be built at discovery time. diff --git a/.github/instructions/pester-supporting-docs/test-data-guide.md b/.github/instructions/pester-supporting-docs/test-data-guide.md index 648c4f4..286f0b0 100644 --- a/.github/instructions/pester-supporting-docs/test-data-guide.md +++ b/.github/instructions/pester-supporting-docs/test-data-guide.md @@ -1,6 +1,6 @@ # Test Data Management Guide -Targets **Pester 6.1+**. +Targets **Pester 6.2+**. **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain text descriptions and standard ASCII characters only. @@ -39,8 +39,10 @@ green. If the source is an empty array instead, Pester 6 fails discovery outrigh Because Pester 6 discovers **one file at a time**, discovery-time data must also be produced by the file that uses it - a helper module imported by another test file is not guaranteed to be loaded. -Import helpers in the same file, or provide them via a `Pester.BeforeContainer.ps1` at the -repository root. +Import helpers in the same file, or provide them from a `Pester.BeforeContainer.ps1`. Top-level code +in that file runs at discovery, which is exactly when `-ForEach` data is built; anything the tests +need at run time goes in its `BeforeAll` - see +[Test Structure Guide](./test-structure-guide.md#shared-bootstrap). ### Test data must be deterministic at discovery time @@ -544,7 +546,6 @@ function Clear-TestDataCache { ```powershell BeforeEach { # Create isolated test data for each test. - # Only ONE BeforeEach and ONE AfterEach per block - duplicates throw in Pester 6. $script:TestUsers = New-TestUsers -Count 3 -Type 'Standard' $script:TestConfig = New-TestConfiguration -Environment 'Testing' } diff --git a/.github/instructions/pester-supporting-docs/test-execution.md b/.github/instructions/pester-supporting-docs/test-execution.md index dacf89a..15dd6d9 100644 --- a/.github/instructions/pester-supporting-docs/test-execution.md +++ b/.github/instructions/pester-supporting-docs/test-execution.md @@ -1,6 +1,6 @@ # Test Execution Guide -Targets **Pester 6.1+** on Windows PowerShell 5.1 or PowerShell 7.4+. +Targets **Pester 6.2+** on Windows PowerShell 5.1 or PowerShell 7.4+. **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain text descriptions and standard ASCII characters only. @@ -28,7 +28,7 @@ param( [string[]]$ExcludeTag = @(), # EXPERIMENTAL: run test files concurrently, one file per runspace. - # Requires PowerShell 7+; falls back to sequential with a warning otherwise. + # Runs on Windows PowerShell 5.1 and PowerShell 7 alike from Pester 6.2. [switch]$Parallel, [int]$ThrottleLimit = 0, # 0 = use all available processors @@ -41,16 +41,17 @@ begin { Write-Host "PowerShell Test Execution Framework" -ForegroundColor Cyan Write-Host "Test Type: $TestType | Environment: $Environment" -ForegroundColor Green - # Pester 6.1 is required - Should-* assertions, New-ShouldAssertion, the parallel - # runner, and Run.Shuffle are not all present in earlier versions + # Pester 6.2 is required - Should-* assertions, New-ShouldAssertion, the parallel + # runner, Run.Shuffle, and BeforeAll in Pester.BeforeContainer.ps1 are not all + # present in earlier versions $pester = Get-Module Pester -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 - if (-not $pester -or $pester.Version -lt [version]'6.1.0') { - Write-Error "Pester 6.1+ is required (found: $(if ($pester) { $pester.Version } else { 'none' })). Install with: Install-Module Pester -MinimumVersion 6.1.0 -Force" + if (-not $pester -or $pester.Version -lt [version]'6.2.0') { + Write-Error "Pester 6.2+ is required (found: $(if ($pester) { $pester.Version } else { 'none' })). Install with: Install-Module Pester -MinimumVersion 6.2.0 -Force" exit 1 } - Import-Module Pester -MinimumVersion 6.1.0 -Force + Import-Module Pester -MinimumVersion 6.2.0 -Force # Ensure output directory exists if (-not (Test-Path $OutputPath)) { @@ -122,10 +123,7 @@ process { # Configure parallel execution if ($Parallel) { - if ($PSVersionTable.PSVersion.Major -lt 7) { - Write-Warning "Run.Parallel requires PowerShell 7+; running sequentially." - } - elseif ($TestType -in 'Performance', 'Integration') { + if ($TestType -in 'Performance', 'Integration') { Write-Warning "$TestType tests should not run in parallel; ignoring -Parallel." } else { @@ -244,8 +242,8 @@ process { } # Report containers that failed to discover. This is a v6 failure mode - # (duplicate setup blocks, empty -ForEach, or a file that cannot be - # discovered independently) and it does NOT show up in FailedCount - + # (empty -ForEach, or a file that cannot be discovered independently) + # and it does NOT show up in FailedCount - # a suite can report 0 failed tests while whole files never ran. if ($result.FailedContainersCount -gt 0) { Write-Host "`nFailed Containers (discovery or setup errors):" -ForegroundColor Red @@ -374,8 +372,8 @@ function Get-TroubleshootingHint { 'Timeout' = 'Increase timeout values or optimize performance. See ./Troubleshooting/Performance/' 'not recognized' = 'Command not found. In Pester 6 each test file must import its own modules - check BeforeAll/BeforeDiscovery.' 'ForEach' = 'Empty -ForEach fails discovery in Pester 6. Build test data in BeforeDiscovery, not BeforeAll.' - 'already defined' = 'Duplicate BeforeAll/BeforeEach/AfterAll/AfterEach in one block throws in Pester 6. Merge them.' 'Assert-Mock' = 'Assert-MockCalled was removed in Pester 6. Use Should -Invoke or Should-Invoke.' + 'to measure or a' = 'Should-BeFasterThan/SlowerThan need a scriptblock or a timespan (Pester 6.2). Pipe the scriptblock, not its result.' } foreach ($keyword in $hints.Keys) { @@ -481,6 +479,7 @@ names in older guidance return `$null` silently. | Counts | `TotalCount`, `PassedCount`, `FailedCount`, `SkippedCount`, `InconclusiveCount`, `NotRunCount` | | Test collections | `Tests`, `Passed`, `Failed`, `Skipped`, `Inconclusive`, `NotRun` | | Per-file results | `Containers` (each has `Item`, `Result`, `Passed`, `ErrorRecord`) | +| Setup files applied | `$container.BeforeContainerFile` - the `Pester.BeforeContainer.ps1` files that ran for that file, outermost first (6.2+) | | Discovery failures | `FailedContainers`, `FailedContainersCount` | | Block-level failures | `FailedBlocks`, `FailedBlocksCount` | | Coverage percent | `CodeCoverage.CoveragePercent` (**not** `CoveredPercent`) | @@ -537,9 +536,10 @@ losing coverage. ## Parallel Test Execution (Experimental) -Pester 6 runs test **files** concurrently, one file per runspace, via PowerShell 7+ -`ForEach-Object -Parallel`. On a multi-core machine this cuts wall-clock time substantially for -large suites. +Pester 6 runs test **files** concurrently, one file per runspace. From 6.2 the workers run on a +runspace pool, which Windows PowerShell 5.1 and PowerShell 7 both have; 6.0 and 6.1 used +`ForEach-Object -Parallel` and so ran sequentially on 5.1. On a multi-core machine this cuts +wall-clock time substantially for large suites. ```powershell $config = New-PesterConfiguration @@ -560,13 +560,22 @@ The run keeps working but emits a **warning** when: | Condition | Reason | | --- | --- | -| Windows PowerShell 5.1 | `ForEach-Object -Parallel` requires PowerShell 7+ | | `ScriptBlock` containers | In-memory containers cannot cross runspaces | | `Run.SkipRemainingOnFailure = 'Run'` | A cross-file stop cannot span runspaces | +| Windows PowerShell 5.1 on Pester 6.0 or 6.1 | Those releases used `ForEach-Object -Parallel`, a PowerShell 7 feature. 6.2 moved to a runspace pool and runs parallel on 5.1 | When every file opts out with `#pester:no-parallel` the run is simply sequential and no warning is printed - there is nothing left to parallelize. +### A Lost File Is an Error + +A worker that died before returning its result used to disappear from the run - the file was not +failed, skipped, or errored, and the run looked green with one file fewer. From 6.2 the run compares +results to the files it sent and throws naming the missing ones. A worker that throws is reported +and the remaining files still report their results, including when the caller runs with +`$ErrorActionPreference = 'Stop'`, which used to abort the loop at the first failing file and drop +everything already finished. + ### Coverage Under Parallel Pester 6.0 collected no coverage in a parallel run. **6.1 collects it**: each worker measures the @@ -616,19 +625,24 @@ each container. ### Prerequisite: Self-Contained Files Parallel only works if each file can be discovered and run on its own, because each worker starts -from a **clean runspace**. Put shared bootstrap in a `Pester.BeforeContainer.ps1` at the repository -root, which Pester dot-sources before every file in both serial and parallel runs: +from a **clean runspace**. Put shared bootstrap in `Pester.BeforeContainer.ps1` files, which Pester +dot-sources before every file in both serial and parallel runs - from 6.2, every one between +`Run.RepoRoot` and the test file's folder. Run-time setup goes in a `BeforeAll`; top-level code in +the file runs at discovery only: ```powershell # Pester.BeforeContainer.ps1, at the repository root -. "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +BeforeAll { + . "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +} ``` The `Run.BeforeContainer` option that also did this was **removed in 6.1**. If a CI job sets it, the assignment now throws. -The file is only picked up from `Run.RepoRoot`, which is resolved from the .NET process working -directory - not `$PWD` - so set it explicitly when the run does not start at the repository root: +The chain starts at `Run.RepoRoot`. When unset, 6.2 resolves it from the session's current location +at run time (6.1 used the .NET process working directory, which `Set-Location` does not move). Set +it explicitly when the run does not start inside the repository: ```powershell $config.Run.RepoRoot = $PSScriptRoot @@ -726,8 +740,8 @@ function Test-TestEnvironment { Sort-Object Version -Descending | Select-Object -First 1 if (-not $pester) { $issues += "Required module missing: Pester" - } elseif ($pester.Version -lt [version]'6.1.0') { - $issues += "Pester 6.1+ required (found $($pester.Version))" + } elseif ($pester.Version -lt [version]'6.2.0') { + $issues += "Pester 6.2+ required (found $($pester.Version))" } # Check test data availability @@ -745,7 +759,8 @@ function Test-TestEnvironment { ### Structural Validation (Discovery-Only Pass) Run discovery without executing anything. This is fast and catches the v6 structural breakages - -duplicate setup blocks, empty `-ForEach` sets, files that cannot be discovered independently: +empty `-ForEach` sets, files that cannot be discovered independently, and from 6.2 a configuration +value of the wrong type: ```powershell function Test-SuiteStructure { diff --git a/.github/instructions/pester-supporting-docs/test-structure-guide.md b/.github/instructions/pester-supporting-docs/test-structure-guide.md index dec8dbd..e3c74b4 100644 --- a/.github/instructions/pester-supporting-docs/test-structure-guide.md +++ b/.github/instructions/pester-supporting-docs/test-structure-guide.md @@ -1,6 +1,6 @@ # Pester Test Structure Guide -Targets **Pester 6.1+**. +Targets **Pester 6.2+**. **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain text descriptions and standard ASCII characters only. @@ -46,16 +46,18 @@ Tests/ ├── Results/ │ ├── Coverage.xml │ └── TestResults.xml +├── Pester.BeforeContainer.ps1 <- optional, applies to Tests/ and below (6.2+) ├── PesterConfiguration.psd1 └── Invoke-Tests.ps1 -Pester.BeforeContainer.ps1 <- optional, at REPOSITORY ROOT (not in Tests/) +Pester.BeforeContainer.ps1 <- optional, at REPOSITORY ROOT - applies to every test file ``` -`Pester.BeforeContainer.ps1` must sit at the repository root - the directory containing `.git`, which -Pester exposes as `Run.RepoRoot`. When present, Pester dot-sources it before **every** test file is -discovered and run, in both serial and parallel runs. As of 6.1 this is the only shared-bootstrap -mechanism; the `Run.BeforeContainer` option was removed. +`Pester.BeforeContainer.ps1` is looked for at the repository root - the directory containing `.git`, +which Pester exposes as `Run.RepoRoot` - and, from 6.2, in every folder between there and the test +file. When present, Pester dot-sources each one before **every** test file below it is discovered +and run, outermost first, in both serial and parallel runs. As of 6.1 this is the only +shared-bootstrap mechanism; the `Run.BeforeContainer` option was removed. ## Test File Isolation (Pester 6) @@ -67,7 +69,7 @@ runspace. discovery-time setup. It cannot rely on a file that happened to be discovered earlier. ```powershell -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } BeforeDiscovery { # Only what is needed to BUILD the test tree: -ForEach data, helper commands @@ -87,21 +89,57 @@ isolated need no changes. ### Shared Bootstrap -When many files need identical setup, put it in one place rather than duplicating it: +When many files need identical setup, put it in one place rather than duplicating it. The setup file +follows the same rule as a test file: top-level code runs during **discovery** only, and what the +tests need at **run** time goes in a `BeforeAll`: ```powershell # Pester.BeforeContainer.ps1 at the repository root + +# Discovery only - keep this only where a -ForEach or BeforeDiscovery needs it Import-Module "$PSScriptRoot/Source/ModuleName.psd1" -Force -. "$PSScriptRoot/Tests/TestHelpers/TestHelpers.ps1" + +# Run time - what the tests themselves need +BeforeAll { + Import-Module "$PSScriptRoot/Source/ModuleName.psd1" -Force + . "$PSScriptRoot/Tests/TestHelpers/TestHelpers.ps1" +} ``` +Before 6.2 everything sat at top level and reached the tests. On 6.2 that shape fails silently: a +function or variable dot-sourced at top level is gone by the time a test runs (the helper call fails +with `CommandNotFoundException`, the variable reads as `$null`), and only `Import-Module` happens to +survive because module state is session-wide. Wrap run-time setup in `BeforeAll`, and keep the +top-level part only where discovery genuinely needs it. + Anchor every path in it to `$PSScriptRoot`. The file runs before each container in both serial and parallel runs, and a relative path would resolve against whatever the working directory happens to -be - which is precisely why the `Run.BeforeContainer` scriptblock option was removed in 6.1. +be - which is precisely why the `Run.BeforeContainer` scriptblock option was removed in 6.1. It runs +once per container, so what it does must be safe to repeat. + +#### Per-folder setup (6.2+) + +A `Pester.BeforeContainer.ps1` in a subfolder applies to that folder and below, after the ones +above it, so each suite carries only what it needs: + +```powershell +# Tests/Unit/Pester.BeforeContainer.ps1 +BeforeAll { $script:Db = 'in-memory' } + +# Tests/Integration/Pester.BeforeContainer.ps1 +BeforeAll { $script:Db = 'real-sql' } +``` + +Each file is dot-sourced into the container's own scope, so `Tests/Integration` never inherits what +`Tests/Unit` set up, whichever runs first. A folder that wants none of the setup above it starts its +file with `#pester:no-inherit`. The result object records which files applied to each container as +`BeforeContainerFile`, outermost first - see +[Pester Configuration Guide](./pester-configuration.md#folder-scoped-setup-62). -If the bootstrap does not appear to run, check `Run.RepoRoot`. It is resolved from the .NET process -working directory rather than `$PWD`, so a run launched from outside the repository looks for the -file in the wrong place and simply finds nothing: +If the bootstrap does not appear to run, check `Run.RepoRoot` - the chain starts there. From 6.2 the +run resolves it from the session's current location when it is unset (6.1 used the .NET process +working directory), so a run launched from outside the repository, or against a checkout with no +`.git` folder, still looks in the wrong place and simply finds nothing: ```powershell $config.Run.RepoRoot = $PSScriptRoot @@ -233,15 +271,14 @@ inside strings. - All external dependencies must be mocked appropriately - Test isolation must be maintained between tests **and between files** - Clean test data and resources in `AfterAll` or `AfterEach` -- Exactly one `BeforeAll`, `BeforeEach`, `AfterAll`, and `AfterEach` per block - duplicates throw - Every `Describe` block carries at least one tag - No `-ForEach` / `-TestCases` expression can evaluate to `$null` or `@()` ### Verifying Structure A discovery-only pass validates the whole suite's structure without paying for a full run. It -surfaces duplicate setup blocks, empty `-ForEach` sets, and files that cannot be discovered -independently: +surfaces empty `-ForEach` sets, files that cannot be discovered independently, and - from 6.2 - a +configuration value of the wrong type: ```powershell $config = New-PesterConfiguration diff --git a/.github/instructions/pester-supporting-docs/unit-test-template.md b/.github/instructions/pester-supporting-docs/unit-test-template.md index 05cea17..5f2b7a0 100644 --- a/.github/instructions/pester-supporting-docs/unit-test-template.md +++ b/.github/instructions/pester-supporting-docs/unit-test-template.md @@ -1,6 +1,6 @@ # Unit Test Template -Targets **Pester 6.1+**. Uses the `Should-*` assertion syntax - see +Targets **Pester 6.2+**. Uses the `Should-*` assertion syntax - see [Assertion Guide](./assertion-guide.md) for the full reference and the v5 mapping table. **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain @@ -16,7 +16,7 @@ text descriptions and standard ASCII characters only. Use this template for all unit tests: ```powershell -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } BeforeDiscovery { # Pester 6 discovers and runs one file at a time, so this file must set up @@ -71,7 +71,7 @@ Describe "Function-Name" -Tag "Unit", "Public" { Context "Core Functionality" { BeforeEach { - # Set up mocks for each test. Only ONE BeforeEach per block - Pester 6 throws on duplicates. + # Set up mocks for each test. Mock External-Dependency { @{ Status = 'Success'; Data = 'MockedData' } } -ModuleName ModuleName @@ -182,10 +182,13 @@ Describe "Function-Name" -Tag "Unit", "Public" { ## Pester 6 Authoring Rules -### One setup block per scope +### Setup blocks compose -Pester 6 **throws** on duplicate `BeforeAll`, `BeforeEach`, `AfterAll`, or `AfterEach` in the same -block. This catches a common copy-paste mistake. Merge them into one. +From 6.2 a block may declare several `BeforeAll`, `BeforeEach`, `AfterAll`, or `AfterEach` blocks. +Setups run in declaration order and teardowns in reverse, so setup can be grouped by what it sets +up - one `BeforeAll` that imports the module, another that builds the fixture - instead of merged +into a single block. 6.0 and 6.1 throw on the second one, so a suite that must still run on those +versions keeps one per block. ### No `param()` block in `It` diff --git a/.github/instructions/pester-supporting-docs/v6-migration.md b/.github/instructions/pester-supporting-docs/v6-migration.md index 3b7dd5c..5659e8b 100644 --- a/.github/instructions/pester-supporting-docs/v6-migration.md +++ b/.github/instructions/pester-supporting-docs/v6-migration.md @@ -15,11 +15,13 @@ apply. ```powershell #Requires -Version 5.1 -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } ``` -These standards target **6.1+**. Nothing in 6.1 breaks a 6.0 suite, so the migration below is the -whole job; see [Moving From 6.0 to 6.1](#moving-from-60-to-61) at the end for what 6.1 adds. +These standards target **6.2+**. Nothing in 6.1 or 6.2 breaks a test file written for 6.0, so the +migration below is the whole job for test files; see [Moving From 6.0 to 6.1](#moving-from-60-to-61) +and [Moving From 6.1 to 6.2](#moving-from-61-to-62) at the end for what changed underneath - +configuration, positional parameters, and the shape of `Pester.BeforeContainer.ps1`. ## Upgrade Checklist @@ -28,30 +30,32 @@ behavior changes that can pass silently and mislead. - [ ] **1. Replace `Assert-MockCalled` and `Assert-VerifiableMock`.** Both were **removed**. Use `Should -Invoke` / `Should -InvokeVerifiable`, or the new `Should-Invoke` / `Should-NotInvoke`. -- [ ] **2. Remove duplicate setup/teardown blocks.** Two `BeforeAll` (or `BeforeEach`, `AfterAll`, - `AfterEach`) in the same block now **throw** instead of being silently allowed. Merge them. -- [ ] **3. Remove `-Focus` and `-Pending`.** `Describe`/`Context`/`It` no longer accept `-Focus`, and +- [ ] **2. Remove `-Focus` and `-Pending`.** `Describe`/`Context`/`It` no longer accept `-Focus`, and the `Focus` property is gone from the result object. `Set-ItResult -Pending` is gone - use `-Skipped` or `-Inconclusive`. Use `-Skip`, tags, or `Filter` to select which tests run. -- [ ] **4. Audit every `-ForEach` / `-TestCases` that can produce an empty set.** `$null` or `@()` +- [ ] **3. Audit every `-ForEach` / `-TestCases` that can produce an empty set.** `$null` or `@()` now **fails discovery** (`Run.FailOnNullOrEmptyForEach`, on by default). Opt out per block or test with `-AllowNullOrEmptyForEach`, or fix the generator so it cannot be empty. -- [ ] **5. Make every test file self-contained.** Discovery and run now happen per file - a module +- [ ] **4. Make every test file self-contained.** Discovery and run now happen per file - a module imported at discovery time in one file is not guaranteed to be loaded when another file is discovered. See below. -- [ ] **6. Check for a literal tag named `None`.** `None` is now a reserved filter value meaning +- [ ] **5. Check for a literal tag named `None`.** `None` is now a reserved filter value meaning "tests with no tags". Rename any real tag called `None`. -- [ ] **7. Review code coverage settings.** Profiler-based coverage is now the default; set +- [ ] **6. Review code coverage settings.** Profiler-based coverage is now the default; set `CodeCoverage.UseBreakpoints = $true` only if you depend on breakpoint-based numbers. The `CoverageGutters` output format was **removed** - use `JaCoCo` or `Cobertura`. -- [ ] **8. Check test and block names containing `<...>`.** Only `<...>` templates are expanded now, +- [ ] **7. Check test and block names containing `<...>`.** Only `<...>` templates are expanded now, and their contents are evaluated as full PowerShell expressions. -- [ ] **9. Check for `*.Tests.ps1` in hidden or dot-prefixed folders.** These are now discovered and +- [ ] **8. Check for `*.Tests.ps1` in hidden or dot-prefixed folders.** These are now discovered and run. Add `Run.ExcludePath` entries for any you do not want picked up. -- [ ] **10. Adopt `Should-*` assertions in new tests.** Optional and additive - see +- [ ] **9. Adopt `Should-*` assertions in new tests.** Optional and additive - see [Assertion Guide](./assertion-guide.md). Do not set `Should.DisableV5 = $true` until the whole suite is migrated. +A second `BeforeAll` (or `BeforeEach`, `AfterAll`, `AfterEach`) in one block threw in 6.0 and 6.1 +and was a migration item. 6.2 allows it again - see +[Moving From 6.1 to 6.2](#moving-from-61-to-62) - so it is no longer on the list. + ## Discovery and Run Now Happen Per File This is the most significant change, and it is invisible for well-isolated suites. @@ -87,15 +91,20 @@ Runtime setup in `BeforeAll` was never affected by this and still works as befor **Shared bootstrap.** When several files need the same setup, put a `Pester.BeforeContainer.ps1` in the repository root (`Run.RepoRoot`). Pester dot-sources it before **every** test file is discovered -and run, in both serial and parallel runs: +and run, in both serial and parallel runs. Run-time setup goes in a `BeforeAll`; the file's top-level +code runs at discovery only (6.2): ```powershell # Pester.BeforeContainer.ps1, at the repository root -. "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +BeforeAll { + . "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +} ``` 6.0 also offered a `Run.BeforeContainer` configuration option for this. It was **removed in 6.1** - -see [Moving From 6.0 to 6.1](#moving-from-60-to-61). The convention file is now the only mechanism. +see [Moving From 6.0 to 6.1](#moving-from-60-to-61). The convention file is now the only mechanism, +and from 6.2 it may also sit in any folder under the root - see +[Moving From 6.1 to 6.2](#moving-from-61-to-62). **Console output changed.** A run prints one `Running tests from N files.` banner, then per-file results, then one grand-total summary. The old `Starting discovery in N files.` / @@ -194,11 +203,11 @@ $config.Run.Parallel = $true $config.Run.ParallelThrottleLimit = 4 # 0 (default) uses all processors ``` -Requires PowerShell 7+ and file-based containers. Falls back to a sequential run **with a warning** -on Windows PowerShell 5.1, for in-memory `ScriptBlock` containers, and when -`Run.SkipRemainingOnFailure = 'Run'`. Coverage under parallel was unsupported in 6.0 and works from -6.1 onward, at the cost of forced breakpoint mode - see -[Test Execution Guide](./test-execution.md). +Requires file-based containers. Runs on PowerShell 7 and, from 6.2, on Windows PowerShell 5.1 (6.0 +and 6.1 fell back to sequential there). Falls back to a sequential run **with a warning** for +in-memory `ScriptBlock` containers and when `Run.SkipRemainingOnFailure = 'Run'`. Coverage under +parallel was unsupported in 6.0 and works from 6.1 onward, at the cost of forced breakpoint mode - +see [Test Execution Guide](./test-execution.md). Opt a single file out with a comment directive parsed like `#requires`: @@ -227,7 +236,7 @@ Get-ChildItem -Recurse -Filter *.Tests.ps1 | Select-String -Pattern '-Focus\b|Set-ItResult\s+-Pending' | Select-Object Path, LineNumber, Line -# 4. Discovery-only pass - surfaces duplicate setup blocks and empty -ForEach without running tests +# 4. Discovery-only pass - surfaces empty -ForEach sets and files that cannot be discovered alone $config = New-PesterConfiguration $config.Run.Path = './Tests' $config.Run.SkipRun = $true @@ -238,8 +247,8 @@ Invoke-Pester -Configuration $config Invoke-Pester -Path ./Tests -TagFilter 'None' ``` -Step 4 is the highest-value check: it walks every file through discovery, so duplicate -`BeforeAll`/`AfterEach` blocks and empty `-ForEach` sets throw there without paying for a full run. +Step 4 is the highest-value check: it walks every file through discovery, so empty `-ForEach` sets +and files that depend on another file having run first fail there without paying for a full run. ## Moving From 6.0 to 6.1 @@ -260,9 +269,11 @@ Neither of these appears in the 6.1.0 release announcement. Check for both befor `The property 'BeforeContainer' cannot be found on this object`. Move the scriptblock body into a `Pester.BeforeContainer.ps1` at the repository root. -- [ ] **A hashtable config loses it silently.** `New-PesterConfiguration -Hashtable` ignores unknown - keys, so a `Run.BeforeContainer` entry in a `PesterConfiguration.psd1` does **not** throw - the - bootstrap simply stops running. Grep for the name rather than relying on the run to tell you. +- [ ] **A hashtable config lost it silently on 6.1.** `New-PesterConfiguration -Hashtable` ignored + unknown keys, so a `Run.BeforeContainer` entry in a `PesterConfiguration.psd1` did **not** + throw - the bootstrap simply stopped running. From 6.2 `Invoke-Pester` warns + `Ignoring configuration keys 'Run.BeforeContainer'`; on 6.1 grep for the name rather than + relying on the run to tell you. - [ ] **`Should-BeEquivalent -StrictOrder` was removed.** It never worked. If a call passes it, the switch was not doing what its name implied - assert collection order with @@ -341,3 +352,158 @@ Turning either on can surface real problems in an existing suite - that is what `Mock.Global` can make a previously-unmocked call start hitting a mock; `Run.Shuffle` fails tests that depended on declaration order. Both may still change before they are declared stable, so pin the behavior you rely on in a dedicated job rather than across the whole suite. + +## Moving From 6.1 to 6.2 + +6.2.0 shipped 2026-09-09. It is additive for test files - no assertion or mocking API a test file +uses was removed or renamed. The changes that can affect an existing suite are in +`Pester.BeforeContainer.ps1`, in the two timing assertions, and in how the configuration treats bad +input. Everything below was verified against an installed 6.2.0. + +```powershell +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } +``` + +### Check Before Upgrading + +- [ ] **Top-level code in `Pester.BeforeContainer.ps1` runs during discovery only.** Anything the + tests need at run time has to be in a `BeforeAll`. A setup file written for 6.1 has no + `BeforeAll` and stops working without a warning - see + [The Setup File Needs a BeforeAll](#the-setup-file-needs-a-beforeall) below. +- [ ] **`Should-BeFasterThan` and `Should-BeSlowerThan` throw on input they cannot measure.** Both + take a `[scriptblock]` to run or a `[timespan]` to compare. On 6.1 a string, a number, or + `$null` fell through and the test passed having asserted nothing; on 6.2 it fails with + `Expected a [scriptblock] to measure or a [timespan] to compare, but got [string] '...'`. A + test that starts failing here was never measuring - fix it to pipe the scriptblock. +- [ ] **A configuration value of the wrong type throws while the configuration is built.** + `Run.Parallel = 'yes'` fails with `Run.Parallel expects a bool, but got the string 'yes'`. + This bites `psd1` and JSON files first, where a boolean written in quotes arrives as a string. + An `int` for a `decimal` is still accepted, and a `$null` value still means "not set". +- [ ] **A key that matches no option is reported.** `Invoke-Pester` warns once: + `WARNING: Ignoring configuration keys 'Run.Paralel', there are no such options`. The list is + also on the object as `$config.GetUnknownKeys()`, which is the thing to gate on in CI since a + warning does not fail a job. Before 6.2 a misspelled key was silently ignored. +- [ ] **Assertions name themselves in the collection-on-`-Expected` error.** The message that read + "is not allowed by this assertion" now reads "is not allowed by Should-Be". Anything matching + on that text sees a new string. +- [ ] **Stray output from a setup file no longer warns.** It cannot escape the container any more, + so a pipeline that grepped for that warning finds nothing. + +### The Setup File Needs a BeforeAll + +On 6.1 everything in `Pester.BeforeContainer.ps1` sat at top level and reached the tests. On 6.2 the +file follows the same rule as a test file: top-level code runs at discovery, `BeforeAll` runs before +the tests. Measured on 6.2 with the 6.1 shape, in both sequential and parallel runs: + +| Top-level statement in the setup file | Visible at discovery | Visible to the tests | +| --- | --- | --- | +| `Import-Module` | yes | yes - module state is session-wide, not because the file ran | +| `. ./Bootstrap.ps1` defining a function | yes | **no** - `CommandNotFoundException` | +| `$script:Var = ...` | yes | **no** - reads as `$null` | + +Nothing warns. Wrap the run-time part: + +```powershell +# before (6.1) +Import-Module "$PSScriptRoot/Source/MyModule.psd1" -Force +. "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" + +# after (6.2) +BeforeAll { + Import-Module "$PSScriptRoot/Source/MyModule.psd1" -Force + . "$PSScriptRoot/Tests/TestHelpers/Bootstrap.ps1" +} +``` + +Keep a top-level copy only for what discovery itself needs - a helper command used inside a +`-ForEach` or `BeforeDiscovery`. `BeforeAll` in a setup file does nothing on 6.1 - none of its setup +reaches discovery or the tests - so one file cannot serve both versions; the version floor moves +with this change. + +### Restriction Lifted + +A block can have as many `BeforeAll`, `AfterAll`, `BeforeEach` and `AfterEach` blocks as you want. +They all run - setups in declaration order, teardowns in reverse - so setup can be grouped by what it +sets up: + +```powershell +Describe 'Get-User' { + BeforeAll { Import-Module "$PSScriptRoot/../src/MyModule.psd1" -Force } + BeforeAll { $script:user = New-TestUser -Name 'jakub' } + AfterAll { Remove-TestUser -Name 'jakub' } + AfterAll { Remove-Module MyModule -Force } +} +``` + +6.0 and 6.1 threw on the second one. A CI hint keyed on that error, or a lint rule enforcing one +block per scope, can go. + +### What Is New + +| Addition | Where | +| --- | --- | +| `Pester.BeforeContainer.ps1` in any folder under `Run.RepoRoot` - every one from the root down applies, outermost first | [Pester Configuration Guide](./pester-configuration.md#folder-scoped-setup-62) | +| `#pester:no-inherit` - a folder's setup file opts out of everything above it | [Pester Configuration Guide](./pester-configuration.md#folder-scoped-setup-62) | +| `$container.BeforeContainerFile` - which setup files applied, on the result object | [Test Execution Guide](./test-execution.md#result-object-reference) | +| `Should-BeString` prints every differing region with line numbers for strings over 10 lines or 120 characters | [Assertion Guide](./assertion-guide.md#long-strings-get-a-real-diff) | +| `$config.GetUnknownKeys()` - configuration keys that matched no option | [Pester Configuration Guide](./pester-configuration.md#dynamic-configuration-loading) | +| `Run.Parallel` on Windows PowerShell 5.1 (experimental) - the runner moved to a runspace pool | [Test Execution Guide](./test-execution.md#parallel-test-execution-experimental) | +| A parallel run throws when a worker's file goes missing, and a failed worker no longer aborts the rest under `$ErrorActionPreference = 'Stop'` | [Test Execution Guide](./test-execution.md#a-lost-file-is-an-error) | +| Failures point at the line that caused them - errors from inside Pester keep their stack trace, and an assertion failing inside a helper reports the helper's caller too | Output only | + +### Behavior Worth Knowing About + +None of these fail an existing suite, but they change what you see: + +- **`Run.RepoRoot` is resolved from the session's current location** when you did not set it. + `New-PesterConfiguration` still shows the .NET process working directory on the object, but + `Invoke-Pester` re-resolves from `$PWD` before the run, so a session that started elsewhere and + then changed into the repository now finds its setup files. Setting it explicitly still wins. +- **`Run.SkipRun` applies the setup-file chain.** A discovery-only pass - the path the VS Code Test + Explorer uses - used to skip the setup files, so a `-ForEach` over data they provided came back + empty in the explorer and populated in a real run. +- **A `BeforeAll` that throws reports the real error.** 6.1 reported _A 'break' or 'continue' + statement with a label that does not match any enclosing loop escaped from your code_ and + discarded the exception, so a database being down read as a misspelled loop label. +- **`Invoke-Pester` keeps its `-1` exit code when it fails internally.** It could overwrite it with + `$null` when the failure came before the run was created, or with `0` when a plugin failed after + an otherwise successful run. +- **Pester imports on PowerShell 7.4.0 to 7.4.5 again.** 6.0 and 6.1 failed there with + `Cannot convert "PesterConfigurationDeserializer" from String to Type`, because the `net8.0` + assembly referenced a newer `System.Management.Automation` than those releases carry. +- **Stack traces from Pester's own C# code no longer carry the build agent's path.** + `D:\a\1\s\src\csharp\Pester\...` became `/_/src/csharp/Pester/...`, and the build is reproducible + as a side effect. +- **The `TestRegistry` retry message is a debug message now.** `IO exception during a TestRegistry + operation, retrying.` no longer appears as a warning when the retry recovers. + +### Verifying The Upgrade + +```powershell +# 1. Find setup files with top-level statements other than BeforeAll/AfterAll - +# those run at discovery only on 6.2 +Get-ChildItem -Recurse -Filter Pester.BeforeContainer.ps1 | ForEach-Object { + $ast = [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$null) + $topLevel = @($ast.EndBlock.Statements | Where-Object { + $first = $_.PipelineElements[0] + -not ($first -is [System.Management.Automation.Language.CommandAst] -and + $first.GetCommandName() -in 'BeforeAll', 'AfterAll') + }) + if ($topLevel.Count -gt 0) { + "$($_.FullName): $($topLevel.Count) top-level statement(s) run at discovery only" + } +} + +# 2. Confirm which setup files each test file actually got +$result = Invoke-Pester -Path ./Tests -PassThru -Output None +$result.Containers | ForEach-Object { "$($_.Item.Name): $($_.BeforeContainerFile -join ', ')" } + +# 3. Find timing assertions whose left side does not end in a scriptblock's closing brace +Get-ChildItem -Recurse -Filter *.Tests.ps1 | + Select-String -Pattern '[^}\s]\s*\|\s*Should-Be(Faster|Slower)Than' | + Select-Object Path, LineNumber, Line + +# 4. Fail on configuration keys that match no option +$config = New-PesterConfiguration -Hashtable (Import-PowerShellDataFile ./Tests/PesterConfiguration.psd1) +if ($config.GetUnknownKeys()) { throw "Unknown keys: $($config.GetUnknownKeys() -join ', ')" } +``` diff --git a/.github/instructions/pester.instructions.md b/.github/instructions/pester.instructions.md index 5d5c5f5..b80944f 100644 --- a/.github/instructions/pester.instructions.md +++ b/.github/instructions/pester.instructions.md @@ -8,9 +8,12 @@ description: 'Creates comprehensive Pester 6 test suites for PowerShell code wit Generate enterprise-grade Pester test suites following these core requirements. -**Target version: Pester 6.1+** on Windows PowerShell 5.1 or PowerShell 7.4+. Pester 6 removed -support for PowerShell 3, 4, 6, and unsupported 7.x. 6.1 is additive over 6.0 - no test file needs -to change to move between them. +**Target version: Pester 6.2+** on Windows PowerShell 5.1 or PowerShell 7.4+. Pester 6 removed +support for PowerShell 3, 4, 6, and unsupported 7.x. 6.1 and 6.2 are additive for test files - no +test file needs to change to move between 6.0, 6.1, and 6.2. The one thing 6.2 changes underneath a +suite is `Pester.BeforeContainer.ps1`: top-level code in it now runs at discovery only, so run-time +setup there must sit in a `BeforeAll` - see +[Moving From 6.1 to 6.2](./pester-supporting-docs/v6-migration.md#moving-from-61-to-62). **NOTE**: Do not use Unicode emojis in any generated code, documentation, or test output. Use plain text descriptions and standard ASCII characters only. @@ -53,8 +56,11 @@ that break existing suites outright: 1. `Assert-MockCalled` and `Assert-VerifiableMock` were **removed** - use `Should -Invoke` / `Should -InvokeVerifiable` (or `Should-Invoke` / `Should-NotInvoke`). -2. Duplicate `BeforeAll`/`BeforeEach`/`AfterAll`/`AfterEach` in the same block now **throw**. -3. `-Focus` and `Set-ItResult -Pending` were **removed**. +2. `-Focus` and `Set-ItResult -Pending` were **removed**. +3. A `-ForEach` / `-TestCases` that evaluates to `$null` or `@()` now **fails discovery**. + +A second `BeforeAll` (or `BeforeEach`, `AfterAll`, `AfterEach`) in one block threw in 6.0 and 6.1. +6.2 allows it again, so it is no longer a migration item. ### Test Requirements Checklist @@ -80,7 +86,7 @@ generated code unless asked; when a project does enable one, these are the conse | Option | Effect | Note | | --- | --- | --- | -| `Run.Parallel` | One test file per runspace | Requires PowerShell 7+ and file-based containers. Coverage works from 6.1 but is forced onto slower breakpoint mode | +| `Run.Parallel` | One test file per runspace | Requires file-based containers. Runs on PowerShell 7 and, from 6.2, on Windows PowerShell 5.1. Coverage works from 6.1 but is forced onto slower breakpoint mode | | `Run.Shuffle` | Randomizes file, block, and test order | Fails tests that depend on declaration order. Prints a seed; `Run.ShuffleSeed` replays it. Opt a file out with `#pester:no-shuffle` | | `Mock.Global` | A mock applies to calls from any module in the runspace | `-ModuleName` becomes a resolution hint, not a scope. Does **not** reinstate fall-through - a `-ParameterFilter` guard still needs a default mock | @@ -115,7 +121,7 @@ files, and under `Run.Parallel` each file is discovered in its own runspace. Each test file must import the modules it needs and perform its own discovery-time setup: ```powershell -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } BeforeDiscovery { # Anything needed to BUILD the test tree (-ForEach data, helper commands) @@ -128,10 +134,22 @@ BeforeAll { } ``` -When several files share bootstrap, put a `Pester.BeforeContainer.ps1` at the repository root rather -than relying on another file having run first. It is dot-sourced before every container. The -`Run.BeforeContainer` option that also did this was **removed in 6.1**; the convention file is the -only mechanism. It fires only when `Run.RepoRoot` points at the directory holding it - see +When several files share bootstrap, put it in a `Pester.BeforeContainer.ps1` rather than relying on +another file having run first. From 6.2 every such file from `Run.RepoRoot` down to the test file's +own folder applies, outermost first, so unit and integration tests can each carry their own setup. +The file follows the same rule as a test file: top-level code runs at **discovery** only, and +anything the tests need at run time goes in a `BeforeAll`: + +```powershell +# Pester.BeforeContainer.ps1 - at the repository root, or in any folder under it +BeforeAll { + Import-Module "$PSScriptRoot/Source/ModuleName.psd1" -Force + . "$PSScriptRoot/Tests/TestHelpers/TestHelpers.ps1" +} +``` + +The `Run.BeforeContainer` option that also did this was **removed in 6.1**; the convention file is +the only mechanism. The chain starts at `Run.RepoRoot` - see [Pester Configuration Guide](./pester-supporting-docs/pester-configuration.md). ### Quick Test Generation Pattern @@ -146,8 +164,9 @@ Describe "Function-Name" -Tag "Unit", "Public" { } ``` -Only one `BeforeAll`, `BeforeEach`, `AfterAll`, and `AfterEach` per block - duplicates throw in -Pester 6. +From 6.2 a block may hold more than one `BeforeAll`, `BeforeEach`, `AfterAll`, or `AfterEach`. +Setups run in declaration order and teardowns in reverse, so group setup by what it sets up rather +than merging unrelated work into one block. 6.0 and 6.1 throw on the second one. ### Tagging diff --git a/.github/prompts/create-test.prompt.md b/.github/prompts/create-test.prompt.md index 18d007f..f245d23 100644 --- a/.github/prompts/create-test.prompt.md +++ b/.github/prompts/create-test.prompt.md @@ -17,13 +17,13 @@ ${selection} - Test type: ${input:testType:Unit,Integration,Performance,Security:Unit} - Coverage target: ${input:coverage:80%,90%,95%:90%} - Environment: ${input:environment:Local,CI/CD,Both:Both} -- Pester version: ${input:pesterVersion:6.1+,5.x:6.1+} +- Pester version: ${input:pesterVersion:6.2+,5.x:6.2+} **Test Requirements:** **1. Modern Test Patterns ✅** -- Use Pester 6.1+ syntax and features. Pester 6 requires Windows PowerShell 5.1 or PowerShell 7.4+; +- Use Pester 6.2+ syntax and features. Pester 6 requires Windows PowerShell 5.1 or PowerShell 7.4+; target 7.6 (LTS) for new work, as 7.4 and 7.5 reach end of support on 10-Nov-2026 - Prefer the `Should-*` assertions (dash, no space) for new test files; classic `Should -Be` remains supported for existing suites