github pester/Pester 6.1.0

7 hours ago

Pester 6.1.0

🙋 Want to share feedback or report a bug? Open an issue
or start a discussion.

The new Should-* assertions are now open for extension: you can write your own typed assertion
with New-ShouldAssertion and it behaves exactly like a built-in one. Alongside that, this release
adds two experimental features worth trying, global mocks and shuffled test order, and a large round
of assertion, output, and mocking fixes.

Pester 6 runs on Windows PowerShell 5.1 and PowerShell 7.4+.

  • What's new?
    • Write your own Should-* assertions with New-ShouldAssertion
    • Sharper assertions
    • Show tags in the console output
    • Skipped data-driven tests get real names
  • Experimental features
    • Global mocks
    • Shuffled test order
    • Parallel runs keep getting better
  • Other improvements and fixes
  • Thank you
  • Questions?

What's new?

Write your own Should-* assertions with New-ShouldAssertion

The Should-* assertions in 6.0.0 were a closed set. Now you can author your own and it gets the same
building blocks a built-in assertion has: pipeline input collection, consistent value formatting, the
diagnostic hint when someone pipes a collection into a value assertion, and the shared failure path
that makes soft assertions and -ParameterFilter work.

You call New-ShouldAssertion once at the top of your function, then use the object it returns. A
passing result is implicit, you only call Fail() when the check does not hold, and the message
supports <expected>, <actual>, <because> and your own <key> tokens:

function Should-BeAwesome {
    [CmdletBinding()]
    param (
        [Parameter(Position = 1, ValueFromPipeline)] $Actual,
        [Parameter(Position = 0)]                    $Expected = 'Awesome',
        [string] $Because
    )

    $assert = New-ShouldAssertion -Caller $PSCmdlet -Actual $Actual -Buffer $Input
    $Actual = $assert.Actual()

    if ($Actual -ne $Expected) {
        $assert.Fail('Expected <expected>,<because> but got <actual>.', @{ Expected = $Expected; Because = $Because })
    }
}

And it is used, and fails, just like a real one:

'Awesome' | Should-BeAwesome              # passes
'meh'     | Should-BeAwesome -Because 'the docs promised' 'Awesome'
# Expected 'Awesome', because the docs promised, but got 'meh'.

-As (Scalar by default, or ExactType, Collection, CollectionItems, None) selects how the
piped input is collected and how the input hint is worded, so a collection assertion reads its input
as a collection just like Should-BeCollection does. Your custom assertion also works inside a mock
-ParameterFilter with no extra work.

Fail() also takes an optional Hint key in its data. It replaces the default input hint when your
assertion has something more specific to say about the failure, and is printed as Hint: <text> like
every other hint.

One packaging note if you ship your assertions to other people. Should is not an approved
PowerShell verb, so a module that exports Should-* functions makes Import-Module print the
unapproved verb warning to everyone who uses it. A manifest with an explicit FunctionsToExport
does not suppress it, and -DisableNameChecking only moves the problem to your users. Name the
function with the approved Assert verb and export a Should-* alias instead, aliases are not verb
checked:

function Assert-BeAwesome { ... }                              # the real function

Set-Alias -Name Should-BeAwesome -Value Assert-BeAwesome
Export-ModuleMember -Function Assert-BeAwesome -Alias Should-BeAwesome

Nothing in Pester keys off the name of the assertion, it all keys off the $PSCmdlet you pass as
-Caller, so it behaves the same when called through the alias. A test file that defines or
dot-sources a Should-* function needs none of this, only modules warn.

Sharper assertions

The new assertion family got a round of fixes that make the messages and the parameters behave
consistently:

  • Should-BeString -NormalizeLineEnding compares strings ignoring the difference between `n and
    `r`n, which is what you want when a file was written on a different platform:

    "a`r`nb" | Should-BeString "a`nb" -NormalizeLineEnding   # passes
  • Should-BeString points its caret at the first differing character, so a long string diff shows
    you exactly where it went wrong instead of making you count.

  • Should-ContainCollection -IgnoreOrder finds the expected items in any order:

    1, 2, 3 | Should-ContainCollection @(3, 1) -IgnoreOrder   # passes
  • Should-Throw reports the real exception type. When an assertion inside the scriptblock throws,
    the message shows the actual exception type rather than Pester's wrapper.

  • Should-Throw -ExceptionMessage points at unescaped wildcards. The message is matched with
    -like, so [ ] * ? are wildcards. When the expected and the actual message are identical except
    for those characters, the failure says so instead of showing two strings that look the same:

    { throw 'value is [1]' } | Should-Throw -ExceptionMessage 'value is [1]'
    # Expected an exception, with message like 'value is [1]' to be thrown, but the message was 'value is [1]'.
    #
    # Hint: -ExceptionMessage matches using wildcards (-like). The messages are identical except for the
    # wildcard characters [ ] * ? in -ExceptionMessage. Escape them with a backtick (`[) or use
    # [System.Management.Automation.WildcardPattern]::Escape() to match them literally.
  • Type assertions honor custom PSTypeNames, so an object you decorated with a synthetic type name
    asserts against that name.

  • Consistency pass: -Actual sits at the same position across the assertions, -Expected is
    mandatory where it always should have been (Should-NotBeString, Should-BeFasterThan,
    Should-BeSlowerThan), Should-Throw -Because is named-only, and -TrimWhitespace is available on
    Should-NotBeString.

  • Formatting a complex object no longer looks like a hang. Values that used to expand into a huge,
    slow tree (a CommandInfo, for example) are now summarised to something short like
    FunctionInfo{Name=Invoke-Pester}.

Show tags in the console output

Output.ShowTags appends the tags of each Describe, Context and It to its output line, which
makes it easy to see what a -Tag / -ExcludeTag filter is actually matching:

$config = New-PesterConfiguration
$config.Output.ShowTags = $true
# Describing Get-Planet [Tags: Slow, Unix]

Skipped data-driven tests get real names

A skipped data-driven test used to show the raw template, Value <_> repeated for every case. Now the
<_> and <key> templates are expanded from the -ForEach data the same way a run test expands them,
so each skipped case has a name you can actually tell apart.

Describe 'd' {
    It 'handles <_>' -Skip -ForEach 'foo', 'bar' { }
}
# [!] handles foo
# [!] handles bar        (was: handles <_> / handles <_>)

Experimental features

These are on by default only when you opt in, and may still change. Try them and tell us what breaks.

Global mocks

A normal mock only applies to calls from the scope where it is defined, or from the module you name
with -ModuleName. To be sure a command like Invoke-WebRequest is never called from any code under
test, you have to know every module that might call it and mock it in each one.

Turn on the experimental Mock.Global option and a mock reaches the command wherever it is called,
from any module or script in the runspace:

$config = New-PesterConfiguration
$config.Mock.Global = $true

You still write the mock exactly as you do today, one mock now covers every caller:

Mock Invoke-WebRequest { '<html />' }
Get-Data                                   # a function in another module that calls Invoke-WebRequest
Should-Invoke Invoke-WebRequest -Times 1

A common use is making sure a command never really runs. Mock it to throw, and combine that with
-ParameterFilter to block only the calls you care about while the rest fall through to the real
command:

# block deleting anything outside TestDrive, from any code under test
Mock Remove-Item { throw 'blocked' } -ParameterFilter { $Path -notlike "$TestDrive*" }

The mock is removed when the test or block that defined it ends, like any other mock, and it is tied
to the run that created it so it cannot leak into a nested Pester-in-Pester run. With the option on,
-ModuleName is only a hint used to resolve the command, not a scope, so your existing mocks keep
working unchanged.

Please turn this on and tell us what happens. We would like Mock.Global to become the default
in v7, and the feedback from this release is what decides that. For most suites we expect turning it
on to change nothing at all, the mocks already cover the calls the tests make. The one place it can
change something is a mocked command that gets called from a module you did not name with
-ModuleName: that call used to reach the real command, and now it gets the mock. If that changes a
result for you, or you see anything else move, please
open an issue. A report that says "turned it on,
nothing changed" is just as useful to us.

Shuffled test order

Tests that quietly depend on running in a fixed order are a common source of "passes on my machine".
Run.Shuffle reorders your test files, and the blocks and tests inside them, so those hidden
dependencies surface:

$config = New-PesterConfiguration
$config.Run.Shuffle = $true

Items are only reordered within their own level, a test never jumps out of its Context. The run
picks a seed and prints it at the start; set Run.ShuffleSeed to that value to replay the exact same
order:

$config.Run.ShuffleSeed = 1234567890   # repeat a specific shuffle

A single file that genuinely must run in order can opt out with a comment:

#pester:no-shuffle
Describe 'ordered steps' { ... }

Parallel runs keep getting better

The experimental parallel runner from 6.0.0 got several rounds of work in 6.1.0:

  • Code coverage is collected across parallel workers, so turning on parallel no longer means losing
    your coverage numbers.
  • Describing / Context headers render in the parallel Detailed output, so the interleaved
    output is readable instead of a flat list.
  • Worker Write-Verbose / Write-Debug output is replayed interleaved with the tests it came from.
  • A concurrent-import crash in Run.Parallel (a thread-unsafe verb patch) was fixed.

Other improvements and fixes

  • Containers that fail during discovery are now reported in the TestResult XML instead of vanishing.
  • A stray unmatched-label break / continue fails the test instead of aborting the whole run.
  • ExcludePath excludes directories, not just files.
  • Code coverage is collected from Invoke-InNewProcess child processes, and a false negative for
    steppable-pipeline proxy functions was fixed.
  • The JUnit testsuite element gets a timestamp attribute.
  • Mocking fixes: commands with OrderedDictionary parameters on PowerShell 7, cmdlets with no
    DefaultParameterSetName, and friendlier Encoding parameter binding.
  • The mock parameter filter serializer no longer throws when a bound parameter's ToString() throws;
    it fails open and keeps the diagnostic instead of taking down the test.
  • -ExpectedMessage on Should -Throw now points out when the expected and actual message are
    identical except for wildcard characters, so a [bracketed] message no longer fails with two
    identical-looking strings.

Full Changelog: 6.0.0...6.1.0

Thank you

Thank you to everyone who filed issues, tried the alphas, and sent fixes for this release.

Questions?

Open an issue or start a
discussion.

🤖

Don't miss a new Pester release

NewReleases is sending notifications on new releases.