Skip to content

Commit 3cd758e

Browse files
authored
test: Add Test-PSBuildPester integration tests and fix latent bugs (#137)
Add tests/Test-PSBuildPester.tests.ps1 (#102). Every invocation runs in a Start-Job subprocess with a pinned inner Pester version, and the scenarios run against both installed Pester majors (5.x and 6.x) to verify the shipped function keeps supporting Pester 5 consumers. Crash fixtures are generated into $TestDrive at runtime, never checked in. Scenarios: healthy suite, failing test (regression #52), BeforeAll crash and discovery crash (#128/#133 gate), NUnit result output, and code coverage output path/format (regression #62). Pester 5.9.0 is installed side by side with 6.0.0 through the new install-only requirements.pester-matrix.psd1; the bootstrap must not import it, because importing two Pester majors into one session crashes with a Pester.dll version conflict. Fix two latent Test-PSBuildPester bugs the tests exposed, test-first: - The finally block called Remove-Module with an empty -Name when the optional ModuleName parameter was omitted, raising a parameter- binding error that -ErrorAction SilentlyContinue cannot suppress. - An unconditional Import-Module Pester -MinimumVersion 5.0.0 loaded the newest installed Pester on top of an already-loaded one. The function now respects a loaded Pester (with a clear error below 5.0.0) and imports the newest installed version only when none is loaded. Closes #102
1 parent 82532a3 commit 3cd758e

7 files changed

Lines changed: 306 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## Unreleased
99

10+
### Fixed
11+
12+
- [**#102**](https://github.com/psake/PowerShellBuild/issues/102)
13+
`Test-PSBuildPester` no longer raises a parameter-binding error from its
14+
cleanup logic when the optional `ModuleName` parameter is not supplied.
15+
- [**#102**](https://github.com/psake/PowerShellBuild/issues/102)
16+
`Test-PSBuildPester` now respects a Pester module that is already loaded in
17+
the session instead of unconditionally importing the newest installed
18+
version on top of it, which crashed with a Pester.dll version conflict when
19+
two Pester versions were installed side by side. When no Pester is loaded,
20+
the newest installed version (5.0.0 minimum) is imported as before, and a
21+
loaded Pester older than 5.0.0 now produces a clear error.
22+
1023
## [0.8.2] 2026-07-08
1124

1225
### Fixed

PowerShellBuild/PowerShellBuild.psm1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ FolderDoesNotExist=Folder does not exist: {0}
2727
PathArgumentMustBeAFolder=The Path argument must be a folder. File paths are not allowed.
2828
UnableToFindModuleManifest=Unable to find module manifest [{0}]. Can't import module
2929
PesterTestsFailed=One or more Pester tests failed
30+
PesterVersionNotSupported=Pester version [{0}] is loaded, but Test-PSBuildPester requires Pester 5.0.0 or newer.
3031
CodeCoverage=Code Coverage
3132
Type=Type
3233
CodeCoverageLessThanThreshold=Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}]

PowerShellBuild/Public/Test-PSBuildPester.ps1

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,15 @@ function Test-PSBuildPester {
6767
[string]$OutputVerbosity = 'Detailed'
6868
)
6969

70-
if (-not (Get-Module -Name Pester)) {
71-
Import-Module -Name Pester -ErrorAction Stop
70+
# Respect an already-loaded Pester so callers can pin a specific version; importing again
71+
# would load the newest installed Pester on top of it, which crashes when two Pester
72+
# versions are installed side by side. Only load Pester ourselves when none is loaded.
73+
$loadedPester = Get-Module -Name Pester
74+
if (-not $loadedPester) {
75+
$loadedPester = Import-Module -Name Pester -MinimumVersion 5.0.0 -ErrorAction Stop -PassThru
76+
}
77+
if ($loadedPester.Version -lt [version]'5.0.0') {
78+
throw ($LocalizedData.PesterVersionNotSupported -f $loadedPester.Version)
7279
}
7380

7481
try {
@@ -84,7 +91,6 @@ function Test-PSBuildPester {
8491

8592
Push-Location -LiteralPath $Path
8693

87-
Import-Module Pester -MinimumVersion 5.0.0
8894
$configuration = [PesterConfiguration]::Default
8995
$configuration.Output.Verbosity = $OutputVerbosity
9096
$configuration.Run.PassThru = $true
@@ -142,6 +148,10 @@ function Test-PSBuildPester {
142148
}
143149
} finally {
144150
Pop-Location
145-
Remove-Module $ModuleName -ErrorAction SilentlyContinue
151+
# ModuleName is optional; Remove-Module with an empty -Name raises a parameter-binding
152+
# error that -ErrorAction SilentlyContinue cannot suppress.
153+
if ($ModuleName) {
154+
Remove-Module -Name $ModuleName -ErrorAction SilentlyContinue
155+
}
146156
}
147157
}

PowerShellBuild/en-US/Messages.psd1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ FolderDoesNotExist=Folder does not exist: {0}
1414
PathArgumentMustBeAFolder=The Path argument must be a folder. File paths are not allowed.
1515
UnableToFindModuleManifest=Unable to find module manifest [{0}]. Can't import module
1616
PesterTestsFailed=One or more Pester tests failed
17+
PesterVersionNotSupported=Pester version [{0}] is loaded, but Test-PSBuildPester requires Pester 5.0.0 or newer.
1718
CodeCoverage=Code Coverage
1819
Type=Type
1920
CodeCoverageLessThanThreshold=Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}]

build.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ if ($Bootstrap.IsPresent) {
4242
}
4343
Import-Module -Name PSDepend -Verbose:$false
4444
Invoke-PSDepend -Path './requirements.psd1' -Install -Import -Force -WarningAction SilentlyContinue
45+
# Install-only, never imported: importing a second Pester major into this session would
46+
# crash with a Pester.dll version conflict. See requirements.pester-matrix.psd1.
47+
Invoke-PSDepend -Path './requirements.pester-matrix.psd1' -Install -Force -WarningAction SilentlyContinue
4548
}
4649

4750
# Execute psake task(s)

requirements.pester-matrix.psd1

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Install-only dependencies. The bootstrap in build.ps1 installs this file WITHOUT importing:
2+
# these modules exist so the Test-PSBuildPester integration tests can pin them inside
3+
# subprocesses, and importing a second Pester major into the bootstrap session would crash
4+
# with a Pester.dll version conflict against the Pester version from requirements.psd1.
5+
@{
6+
PSDependOptions = @{
7+
Target = 'CurrentUser'
8+
}
9+
# Newest Pester 5.x, installed side by side with the pinned 6.x so the shipped
10+
# Test-PSBuildPester function is verified against both supported majors.
11+
PesterLegacy = @{
12+
Name = 'Pester'
13+
Version = '5.9.0'
14+
Parameters = @{
15+
SkipPublisherCheck = $true
16+
}
17+
}
18+
}

tests/Test-PSBuildPester.tests.ps1

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# Integration tests for Test-PSBuildPester (psake/PowerShellBuild#102).
2+
#
3+
# Test-PSBuildPester wraps Invoke-Pester, so these tests are Pester-testing-Pester. Every
4+
# invocation runs in a Start-Job subprocess: two Pester versions cannot coexist in one session,
5+
# and the subprocess lets each test pin the inner Pester version independently of the outer
6+
# framework. The scenarios run against every installed Pester major (5.x and 6.x) to verify the
7+
# shipped function keeps supporting Pester 5 consumers.
8+
#
9+
# The crash fixtures are generated into $TestDrive at runtime, never checked in, so the
10+
# repository's own Pester run can never discover them (see #97 for the convention).
11+
12+
BeforeDiscovery {
13+
# Newest installed Pester of each supported major version. CI installs 6.x (Pester) and
14+
# 5.x (PesterLegacy) via requirements.psd1; locally, absent majors simply produce fewer
15+
# matrix legs.
16+
$script:innerPesterVersions = @(
17+
foreach ($majorVersion in 5, 6) {
18+
$newestOfMajor = Get-Module -Name 'Pester' -ListAvailable |
19+
Where-Object { $_.Version.Major -eq $majorVersion } |
20+
Sort-Object -Property 'Version' -Descending |
21+
Select-Object -First 1
22+
if ($newestOfMajor) {
23+
$newestOfMajor.Version.ToString()
24+
}
25+
}
26+
)
27+
}
28+
29+
Describe 'Test-PSBuildPester' {
30+
31+
BeforeAll {
32+
$script:moduleRoot = Split-Path -Path $PSScriptRoot -Parent
33+
$script:builtModulePath = [IO.Path]::Combine($script:moduleRoot, 'Output', 'PowerShellBuild')
34+
35+
Import-Module -Name ([IO.Path]::Combine($PSScriptRoot, 'fixtures', 'FixtureHelpers.psm1')) -Force
36+
37+
# Runs Test-PSBuildPester in a subprocess with a pinned inner Pester version and reports
38+
# what happened. Returns an object with Threw, ErrorMessage, and the Pester version that
39+
# was loaded in the subprocess after the call.
40+
function script:Invoke-TestPSBuildPesterJob {
41+
param(
42+
[string]$InnerPesterVersion,
43+
[string]$Path,
44+
[hashtable]$AdditionalParameters = @{}
45+
)
46+
47+
$job = Start-Job -ScriptBlock {
48+
param($innerPesterVersion, $builtModulePath, $path, $additionalParameters)
49+
50+
Import-Module -Name 'Pester' -RequiredVersion $innerPesterVersion -ErrorAction Stop
51+
Import-Module -Name $builtModulePath -Force -ErrorAction Stop
52+
53+
$testPSBuildPesterParameters = @{
54+
Path = $path
55+
OutputVerbosity = 'None'
56+
ErrorAction = 'Stop'
57+
}
58+
foreach ($key in $additionalParameters.Keys) {
59+
$testPSBuildPesterParameters[$key] = $additionalParameters[$key]
60+
}
61+
62+
$threw = $false
63+
$errorMessage = $null
64+
try {
65+
Test-PSBuildPester @testPSBuildPesterParameters
66+
} catch {
67+
$threw = $true
68+
$errorMessage = $_.Exception.Message
69+
}
70+
71+
[PSCustomObject]@{
72+
Threw = $threw
73+
ErrorMessage = $errorMessage
74+
LoadedPesterVersions = @((Get-Module -Name 'Pester').Version.ToString())
75+
}
76+
} -ArgumentList $InnerPesterVersion, $script:builtModulePath, $Path, $AdditionalParameters
77+
78+
$jobResult = $job | Wait-Job | Receive-Job
79+
Remove-Job -Job $job -Force
80+
$jobResult
81+
}
82+
83+
# Scenario directories, generated at runtime.
84+
$script:healthyPath = Join-Path -Path $TestDrive -ChildPath 'healthy'
85+
$script:failingTestPath = Join-Path -Path $TestDrive -ChildPath 'failingtest'
86+
$script:beforeAllCrashPath = Join-Path -Path $TestDrive -ChildPath 'beforeallcrash'
87+
$script:discoveryCrashPath = Join-Path -Path $TestDrive -ChildPath 'discoverycrash'
88+
$script:coveragePath = Join-Path -Path $TestDrive -ChildPath 'coverage'
89+
$script:outputPath = Join-Path -Path $TestDrive -ChildPath 'out'
90+
foreach ($directory in @(
91+
$script:healthyPath
92+
$script:failingTestPath
93+
$script:beforeAllCrashPath
94+
$script:discoveryCrashPath
95+
$script:coveragePath
96+
$script:outputPath
97+
)) {
98+
New-Item -Path $directory -ItemType Directory -Force > $null
99+
}
100+
101+
Set-Content -Path (Join-Path -Path $script:healthyPath -ChildPath 'Healthy.tests.ps1') -Value @'
102+
Describe 'Healthy suite' {
103+
It 'passes' {
104+
1 | Should -Be 1
105+
}
106+
}
107+
'@
108+
109+
Set-Content -Path (Join-Path -Path $script:failingTestPath -ChildPath 'FailingTest.tests.ps1') -Value @'
110+
Describe 'Suite with a failing test' {
111+
It 'fails' {
112+
1 | Should -Be 2
113+
}
114+
}
115+
'@
116+
117+
Set-Content -Path (Join-Path -Path $script:beforeAllCrashPath -ChildPath 'BeforeAllCrash.tests.ps1') -Value @'
118+
Describe 'Suite with a broken setup' {
119+
BeforeAll {
120+
throw 'BeforeAll exploded'
121+
}
122+
123+
It 'never executes' {
124+
1 | Should -Be 1
125+
}
126+
}
127+
'@
128+
129+
Set-Content -Path (Join-Path -Path $script:discoveryCrashPath -ChildPath 'DiscoveryCrash.tests.ps1') -Value @'
130+
throw 'file exploded during discovery'
131+
132+
Describe 'Unreachable suite' {
133+
It 'is never discovered' {
134+
1 | Should -Be 1
135+
}
136+
}
137+
'@
138+
139+
# Coverage scenario: tests exercising the fixture module, with coverage measured on the
140+
# fixture's public functions.
141+
$script:fixturePath = Copy-PSBuildTestFixture -Destination $TestDrive
142+
$fixtureManifestPath = Join-Path -Path $script:fixturePath -ChildPath 'PSBuildTestFixture.psd1'
143+
Set-Content -Path (Join-Path -Path $script:coveragePath -ChildPath 'Coverage.tests.ps1') -Value @"
144+
BeforeAll {
145+
Import-Module -Name '$fixtureManifestPath' -Force
146+
}
147+
148+
Describe 'Coverage target' {
149+
It 'calls Get-Widget' {
150+
(Get-Widget -Name 'Sprocket').Name | Should -Be 'Sprocket'
151+
}
152+
}
153+
"@
154+
}
155+
156+
AfterAll {
157+
Remove-Module -Name 'FixtureHelpers' -Force -ErrorAction SilentlyContinue
158+
}
159+
160+
Context 'with inner Pester <_>' -ForEach $script:innerPesterVersions {
161+
162+
BeforeAll {
163+
$script:innerVersion = $_
164+
}
165+
166+
It 'succeeds for a healthy suite' {
167+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:healthyPath
168+
169+
$result.Threw | Should -BeFalse
170+
}
171+
172+
It 'fails the build when a test fails' {
173+
# Regression: #52
174+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:failingTestPath
175+
176+
$result.Threw | Should -BeTrue
177+
$result.ErrorMessage | Should -Match 'Pester tests failed'
178+
}
179+
180+
It 'fails the build when a setup block throws' {
181+
# Regression: #128 / #133 (FailedCount alone misses failed blocks)
182+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:beforeAllCrashPath
183+
184+
$result.Threw | Should -BeTrue
185+
$result.ErrorMessage | Should -Match 'Pester tests failed'
186+
}
187+
188+
It 'fails the build when a test file errors during discovery' {
189+
# Regression: #128 / #133 (FailedCount alone misses failed containers)
190+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:discoveryCrashPath
191+
192+
$result.Threw | Should -BeTrue
193+
$result.ErrorMessage | Should -Match 'Pester tests failed'
194+
}
195+
196+
It 'writes test results to the requested output path' {
197+
$testResultsPath = Join-Path -Path $script:outputPath -ChildPath "testResults-$script:innerVersion.xml"
198+
$additionalParameters = @{
199+
OutputPath = $testResultsPath
200+
}
201+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:healthyPath -AdditionalParameters $additionalParameters
202+
203+
$result.Threw | Should -BeFalse
204+
$testResultsPath | Should -Exist
205+
}
206+
207+
It 'writes code coverage in the requested format to the requested path' {
208+
# Regression: #62
209+
$coverageOutputPath = Join-Path -Path $script:outputPath -ChildPath "coverage-$script:innerVersion.xml"
210+
$additionalParameters = @{
211+
CodeCoverage = $true
212+
CodeCoverageFiles = @(Join-Path -Path $script:fixturePath -ChildPath 'Public/*.ps1')
213+
CodeCoverageOutputFile = $coverageOutputPath
214+
CodeCoverageOutputFileFormat = 'JaCoCo'
215+
}
216+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:coveragePath -AdditionalParameters $additionalParameters
217+
218+
$result.Threw | Should -BeFalse
219+
$coverageOutputPath | Should -Exist
220+
[xml]$coverageReport = Get-Content -Path $coverageOutputPath -Raw
221+
$coverageReport.report | Should -Not -BeNullOrEmpty
222+
}
223+
}
224+
225+
# BeforeDiscovery variables are not visible during the run phase, so the discovered version
226+
# list is handed to the run phase through -ForEach.
227+
Context 'Regressions independent of the inner Pester version' -ForEach @(
228+
@{ AvailableVersions = $script:innerPesterVersions }
229+
) {
230+
231+
BeforeAll {
232+
$script:newestInnerVersion = $AvailableVersions | Select-Object -Last 1
233+
$script:oldestInnerVersion = $AvailableVersions | Select-Object -First 1
234+
}
235+
236+
It 'does not error when ModuleName is not provided' {
237+
# Regression: the finally block called Remove-Module with an empty -Name, which
238+
# raised a parameter-binding error that -ErrorAction SilentlyContinue cannot
239+
# suppress.
240+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:newestInnerVersion -Path $script:healthyPath
241+
242+
$result.Threw | Should -BeFalse
243+
$result.ErrorMessage | Should -BeNullOrEmpty
244+
}
245+
246+
It 'honors the Pester version that is already loaded' -Skip:($script:innerPesterVersions.Count -lt 2) {
247+
# Regression: an unconditional Import-Module Pester -MinimumVersion 5.0.0 loaded the
248+
# newest installed Pester on top of an already-loaded older one, which crashes with a
249+
# Pester.dll version conflict when 5.x and 6.x are installed side by side.
250+
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:oldestInnerVersion -Path $script:healthyPath
251+
252+
$result.Threw | Should -BeFalse
253+
$result.LoadedPesterVersions | Should -Be @($script:oldestInnerVersion)
254+
}
255+
}
256+
}

0 commit comments

Comments
 (0)