Skip to content

Five tests pass without being able to fail #219

Description

@tablackburn

A bounded list, not an umbrella. #17 was closed today for being open-ended, and this is deliberately not that: five named tests or test groups, each with the specific reason it cannot fail and the measurement that shows it. Nothing here is a request to "add more tests".


1. tests/New-PSBuildFileCatalog.tests.ps1 — two tests swallow any failure that is not the expected one

Calls New-FileCatalog with correct parameters and Returns a FileInfo object both wrap the real call like this:

try {
    $result = New-PSBuildFileCatalog -ModulePath $testModulePath -CatalogFilePath $catalogPath -CatalogVersion 2
    $result | Should -Not -BeNullOrEmpty
    Test-Path $catalogPath | Should -BeTrue
} catch {
    # If New-FileCatalog isn't available, just verify the function exists and accepts the params
    if ($_.Exception.Message -match 'New-FileCatalog') {
        $command = Get-Command New-PSBuildFileCatalog
        $command.Parameters.ContainsKey('CatalogVersion') | Should -BeTrue
    }
}

If the call fails for any reason whose message does not mention New-FileCatalog — a denied path, a locked file, a changed parameter name, an operating system error — the catch runs, the if is false, and the test passes having asserted nothing at all. Measured with a probe that throws Access to the path is denied. from that position: Tests Passed: 1, Failed: 0.

On this machine the happy path is currently taken and coverage of New-PSBuildFileCatalog.ps1 is 15 of 15 commands, so the tests do work today. The defect is that they are constructed to stop working silently.

A third test, Defaults CatalogVersion to 2 (SHA256), asserts nothing about the default it is named for. Its own comment admits this:

$parameter = $command.Parameters['CatalogVersion']
# The default should be set in the function, we'll check by the ValidateRange attribute
$parameter | Should -Not -BeNullOrEmpty

$parameter is the ParameterMetadata for a parameter the previous test already established exists. The default value of 2 is never read.

Fix shape: drop the try/catch and let a real failure be a real failure — the containing context is already -Skip: guarded for non-Windows, which is the only condition the catch was defending against. For the third test, read the default off the function's abstract syntax tree, or assert that the catalog file the function produces is SHA256.


2. tests/Test-PSBuildScriptAnalysis.tests.ps1 — full instruction coverage, no message ever asserted

43 tests, 55 of 55 commands covered. The assertions are 8 bare Should -Throw and 12 bare Should -Not -Throw, with zero -ExpectedMessage and zero -Because anywhere in the file. Test-PSBuildScriptAnalysis has five throw sites drawing on three localized messages, and no test can tell them apart.

Measured by swapping two of the three messages in the module's localized data — ScriptAnalyzerErrors and ScriptAnalyzerIssues — so the Error threshold now reports "issues" and the Any threshold now reports "errors":

RESULT with swapped messages: Passed=43 Failed=0

That matters because the message is what a consumer sees. The three are not interchangeable: ScriptAnalyzerErrors means "your build has error-severity findings", ScriptAnalyzerWarnings covers the Warning and Information thresholds, and ScriptAnalyzerIssues means "any diagnostic at all was returned, at the Any threshold you asked for". A consumer reading "One or more ScriptAnalyzer errors were found!" after configuring SeverityThreshold = 'Any' would go looking for errors that are not there.

Honourable exception, and the model to copy: the three tests in Analyzer rule crash retry use Should -Invoke -CommandName 'Invoke-ScriptAnalyzer' -Times N -Exactly to pin the retry count at 2, 3, and 1 respectively. Those are the strongest tests in the suite — they assert a specific observable count rather than the absence of an exception, and they would fail on exactly the regression they were written for. Analyzes the supplied path recursively is the same shape with a -ParameterFilter.

Fix shape: the file already imports the module, so the $LocalizedData values can be read and used as Should -Throw -ExpectedMessage. Adding the expected message to the eight throwing tests is a mechanical change and would make all five sites distinguishable.


3. tests/Build-PSBuildModule.tests.ps1 — two "Emits no warning" assertions that are structurally unfalsifiable

Line 265  Context 'Building without compilation'                        It 'Emits no warning'
Line 557  Context 'Building without compilation from a scaffold loader' It 'Emits no warning'

Both assert $script:buildWarning | Should -BeNullOrEmpty after a build with no -Compile. Every Write-Warning in Build-PSBuildModule — the Export-ModuleMember warning at line 174 and NoScriptsToCompile at line 214 — is inside if ($Compile.IsPresent), which begins at line 140 and closes into the else at line 253. A non-compile build cannot emit either one, so neither assertion can fail no matter what happens to the function.

The two sibling assertions in compile contexts are fine: Emits no warning for a root module that does not call Export-ModuleMember (line 381) and Emits no warning for a mention inside a block comment (line 582) both run with -Compile, where the warning is reachable, and the second one specifically pins that a comment mentioning Export-ModuleMember is not reported as a call.

All four arrived in #205, merged earlier today alongside the Export-ModuleMember warning itself. This is not a criticism of that change — it is what happens when a "does not warn" assertion is copied into a context where warning was never possible.

Fix shape: delete the two non-compile assertions, or give them something falsifiable to assert. The genuinely interesting claim for the non-compile path is the one the comment at line 535 makes — "nothing is wrong with the loader; compilation is what breaks it" — and that is already asserted by Builds a module that exports its public functions immediately above it.


4. tests/Build-PSBuildHelp.tests.ps1 — the HelpInfoUri guard test would pass with the guard deleted

declines when the manifest declares no HelpInfoUri (line 250) strips HelpInfoUri from a scenario manifest, calls Build-PSBuildUpdatableHelp, and asserts:

$result.Threw | Should -BeFalse
$scenario.UpdatableHelpPath | Should -Not -Exist

The nouri scenario never has Build-PSBuildMarkdown run against it, and tests/fixtures/PSBuildTestFixture contains no docs directory, so DocsPath does not exist. Get-PSBuildHelpLocale finds nothing, $cabinetWork is empty, and the function returns at if (-not $cabinetWork) { return } — which is after the HelpInfoUri guard but still before OutputPath is created.

Measured, running the same shape twice with only the manifest differing:

Case 1 (HelpInfoUri PRESENT, guard does not fire, no docs tree): OutputPath exists = False
Case 2 (HelpInfoUri ABSENT,  guard fires):                       OutputPath exists = False

Both assertions hold identically either way. The test also never asserts the HelpInfoUriRequired warning, which is the guard's only observable effect and the thing that tells a consumer what to fix.

Fix shape: give the scenario a real docs tree — generate markdown for it the way the cab context does — so that removing the guard would actually produce a cabinet, and assert on the warning as well as on the absent output.


5. tests/Help.tests.ps1 — the link-validation block currently generates no tests

if ($helpLinks) {
    It "Help link <_> is valid" -ForEach $helpLinks {
        (Invoke-WebRequest -Uri $_ -UseBasicParsing).StatusCode | Should -Be '200'
    }
}

No command in the module has a .LINK section. Measured across all twelve exported commands: the link count is 0 for every one, so this block expands to zero tests on every run, and the guard makes that invisible — a suite reporting hundreds of passing help tests contains no link check at all. If a .LINK is ever added and later removed, its coverage disappears the same way.

This is the mildest item in the list, and the guard itself is not wrong — its comment explains that it preserves Pester 5's empty -ForEach behaviour on Pester 6, which is a real problem it solves correctly.

Explicitly not claimed: the neighbouring if ($commandParameters) guard is not a hole. $commandParameters is derived from the command's own parameter metadata rather than from its help, so the block skips only a command that genuinely has no parameters, and the parameter-help assertions inside it do go red when help is missing — measured, twelve failures on Expected a value, but got $null or empty at line 167 when help is unavailable. An earlier reading of this file claimed that total help loss passes green; it does not, and Help is not auto-generated, Has description, and Has example code all fail in that case.

Fix shape: either add .LINK sections to the public commands so the block does something, or drop the block and record that the module does not use .LINK. A one-line note either way is enough.


Why these five, and not a general "improve the tests" issue

Each of the five is a test that reports success while being unable to report failure, which is worse than no test: it occupies the slot where real coverage would go, and it tells a reader the behaviour is checked. Items 2 and 3 are the ones with live consequences — a wrong analyzer message reaches consumers directly, and item 3 sits in a file that is otherwise among the strongest in the suite. Item 5 is a note rather than a defect, and is included so the list is complete rather than because it is urgent.

Related: #17 (closed today as too open-ended — this list is scoped to avoid the same fate), #205 (which added item 3's contexts and much of the surrounding good coverage), #96 (which added the analyzer test file), #149 (which added the help test file).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions