Skip to content

Releases: pester/Pester

6.2.0

Choose a tag to compare

@nohwnd nohwnd released this 09 Sep 19:46
98e56d6

Pester 6.2.0

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

Pester.BeforeContainer.ps1 grew from one file at the repository root into a chain that
follows your folder structure, so unit tests and integration tests can each have their own
setup without repeating it in every test file. A block can have more than one BeforeAll
now, so setup can be grouped by what it sets up instead of merged into a single block.
Should-BeString prints a real diff when a long string does not match. The configuration
now tells you when you handed it a value it cannot use, instead of quietly ignoring it. And
the experimental parallel runner works on Windows PowerShell 5.1, which is the slowest
edition and the one that needed it most.

What's new?

Setup that follows your folders

6.1.0 shipped Pester.BeforeContainer.ps1, a setup file that runs before every test file,
including in parallel runs where each worker starts from a clean runspace. Only the one at
Run.RepoRoot was used though, so a repository where unit tests and integration tests need
different setup had to cram both into that single file, or repeat the setup in every test
file.

Now every Pester.BeforeContainer.ps1 from Run.RepoRoot down to the test file's own folder
is applied, outermost first:

reporoot/Pester.BeforeContainer.ps1                 <- applies to everything
reporoot/tests/Pester.BeforeContainer.ps1           <- applies to tests/ and below
reporoot/tests/unit/Pester.BeforeContainer.ps1      <- applies to tests/unit only
reporoot/tests/integration/Pester.BeforeContainer.ps1

The root file holds common setups and teardowns that every test needs:

# reporoot/Pester.BeforeContainer.ps1
BeforeAll {
    Import-Module $PSScriptRoot/src/MyModule.psd1 -Force
    . $PSScriptRoot/tests/helpers/Assertions.ps1
}

tests/ holds the helpers only tests use, and each suite holds what only it needs:

# reporoot/tests/Pester.BeforeContainer.ps1
BeforeAll {
    function New-TestUser { param($Name) [pscustomobject]@{ Name = $Name } }
}
# reporoot/tests/unit/Pester.BeforeContainer.ps1
BeforeAll { $script:Db = 'in-memory' }
# reporoot/tests/integration/Pester.BeforeContainer.ps1
BeforeAll { $script:Db = 'real-sql' }

A test file in tests/unit gets the root setup, the tests/ setup and the tests/unit
setup, in that order. A test file in tests/integration gets the root setup, the tests/
setup and real-sql. It does not get in-memory, whichever file happens to run first:

# reporoot/tests/unit/Get-User.Tests.ps1
Describe 'Get-User' {
    It 'uses the in-memory database' {
        $script:Db | Should-BeString 'in-memory'          # from tests/unit
        (New-TestUser -Name 'jakub').Name | Should-BeString 'jakub'   # from tests
    }
}

The setup files are dot-sourced into the container's own scope now, not into the run session
state. That is what makes the folders actually scope anything. Before this, setup dot-sourced
for one file stayed visible to every container after it, so a file in tests/integration
would silently inherit whatever tests/unit had set up, and the result depended on run order.

A folder opts out of everything above it with #pester:no-inherit, the same meaning
root = true has in an .editorconfig. Useful for something like doc tests that need their
own cheap setup and should not pay for the expensive one:

# reporoot/tests/docs/Pester.BeforeContainer.ps1
#pester:no-inherit
BeforeAll { Import-Module $PSScriptRoot/../../src/MyModule.psd1 }

The whole chain works in a sequential run and in a parallel run. In parallel the chain is
resolved once in the parent and the paths are handed to the workers, because a worker is a
separate runspace and cannot share the cache. The files run before every container, so what
they do has to be safe to run more than once.

Run.RepoRoot is where the chain starts, and it is found for you by walking up from the
directory you are in until a .git folder shows up. That walk used to start from the process
working directory, which Set-Location does not change, so a session that started somewhere
else and then changed directory into a repository got a root pointing at the old place and
none of the setup files applied, with nothing to say why. It starts from the location the
session is actually in now. Set Run.RepoRoot yourself when your tests do not live in a git
repository, or when the root is somewhere other than where .git is.

Which files apply is a property of the directory, not of the container, so the run shares one
cache keyed by directory. Each directory is checked on disk once per run and each setup file
is tokenized once per run, however many test folders sit below them. On a tree with 60 test
folders and a 26 KB root setup file that is 23.6 ms instead of 845 ms.

A container reports which setup files applied to it, outermost first, in the order they ran.
The list is on the container in the result object, as BeforeContainerFile:

$result = Invoke-Pester -Path ./tests -PassThru
foreach ($container in $result.Containers) {
    "$($container.Item.Name):"
    $container.BeforeContainerFile | ForEach-Object { "    $_" }
}
D.Tests.ps1:
    <root>/tests/docs/Pester.BeforeContainer.ps1
U.Tests.ps1:
    <root>/Pester.BeforeContainer.ps1
    <root>/tests/Pester.BeforeContainer.ps1
    <root>/tests/unit/Pester.BeforeContainer.ps1

D.Tests.ps1 sits under a folder marked #pester:no-inherit, so only its own file applied. The
folder tree cannot tell you that, which is why the list is on the result.

Requested by @johlju in #2772, where he
estimated it removes around a thousand lines of duplication in SqlServerDsc.

See Behavior changes below, there are three.

Should-BeString shows you the whole diff

Comparing a long string used to give you a caret under the first character that differed.
That answers "one thing changed". Comparing a snapshot, a rendered template or a config file
is usually "many things changed", and a scan that stops at the first difference makes you fix
them one run at a time.

Should-BeString now finds every region that differs and prints them with context:

$expected = Get-Content ./expected/package.json -Raw
$actual   = Get-Content ./out/package.json -Raw
$actual | Should-BeString $expected
Expected strings to be the same, but they were different.
Expected length: 234
Actual length:   241
Expected 16 line(s), actual 16 line(s).
2 regions differ.

   1  1 |   {
   2  2 |     "name": "widget",
   3    | -   "version": "1.2.0",
      3 | +   "version": "1.3.0",
   4    | -   "license": "MIT",
      4 | +   "license": "Apache-2.0",
   5  5 |     "main": "index.js",
   6  6 |     "scripts": {
  ...
   9  9 |     },
  10 10 |     "dependencies": {
  11    | -     "left-pad": "^1.3.0"
     11 | +     "left-pad": "^1.3.1"
  12 12 |     },
  13 13 |     "engines": {
  14    | -     "node": ">=18"
     14 | +     "node": ">=20"
  15 15 |     }
  16 16 |   }

Expected line numbers on the left, actual line numbers on the right. They are separate columns
so it stays readable when lines are added or removed and the two sides stop lining up:

   1  1 |   Describe 'Api' {
      2 | +     BeforeAll {
      3 | +         ...
Read more

6.2.0-alpha2

6.2.0-alpha2 Pre-release
Pre-release

Choose a tag to compare

@nohwnd nohwnd released this 05 Sep 15:12
c2ca84f

Pester 6.2.0-alpha2

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

This is a prerelease. Install it with Install-Module Pester -AllowPrerelease.

Pester.BeforeContainer.ps1 grew from one file at the repository root into a chain that
follows your folder structure, so unit tests and integration tests can each have their own
setup without repeating it in every test file. Should-BeString prints a real diff when a
long string does not match. The configuration now tells you when you handed it a value it
cannot use, instead of quietly ignoring it. And the experimental parallel runner works on
Windows PowerShell 5.1, which is the slowest edition and the one that needed it most.

What's new?

Setup that follows your folders

6.1.0 shipped Pester.BeforeContainer.ps1, a setup file that runs before every test file,
including in parallel runs where each worker starts from a clean runspace. Only the one at
Run.RepoRoot was used though, so a repository where unit tests and integration tests need
different setup had to cram both into that single file, or repeat the setup in every test
file.

Now every Pester.BeforeContainer.ps1 from Run.RepoRoot down to the test file's own folder
is applied, outermost first:

reporoot/Pester.BeforeContainer.ps1                 <- applies to everything
reporoot/tests/Pester.BeforeContainer.ps1           <- applies to tests/ and below
reporoot/tests/unit/Pester.BeforeContainer.ps1      <- applies to tests/unit only
reporoot/tests/integration/Pester.BeforeContainer.ps1

The root file holds common setups and teardowns that every test needs:

# reporoot/Pester.BeforeContainer.ps1
BeforeAll {
    Import-Module $PSScriptRoot/src/MyModule.psd1 -Force
    . $PSScriptRoot/tests/helpers/Assertions.ps1
}

tests/ holds the helpers only tests use, and each suite holds what only it needs:

# reporoot/tests/Pester.BeforeContainer.ps1
BeforeAll {
    function New-TestUser { param($Name) [pscustomobject]@{ Name = $Name } }
}
# reporoot/tests/unit/Pester.BeforeContainer.ps1
BeforeAll { $script:Db = 'in-memory' }
# reporoot/tests/integration/Pester.BeforeContainer.ps1
BeforeAll { $script:Db = 'real-sql' }

A test file in tests/unit gets the root setup, the tests/ setup and the tests/unit
setup, in that order. A test file in tests/integration gets the root setup, the tests/
setup and real-sql. It does not get in-memory, whichever file happens to run first:

# reporoot/tests/unit/Get-User.Tests.ps1
Describe 'Get-User' {
    It 'uses the in-memory database' {
        $script:Db | Should-BeString 'in-memory'          # from tests/unit
        (New-TestUser -Name 'jakub').Name | Should-BeString 'jakub'   # from tests
    }
}

The setup files are dot-sourced into the container's own scope now, not into the run session
state. That is what makes the folders actually scope anything. Before this, setup dot-sourced
for one file stayed visible to every container after it, so a file in tests/integration
would silently inherit whatever tests/unit had set up, and the result depended on run order.

A folder opts out of everything above it with #pester:no-inherit, the same meaning
root = true has in an .editorconfig. Useful for something like doc tests that need their
own cheap setup and should not pay for the expensive one:

# reporoot/tests/docs/Pester.BeforeContainer.ps1
#pester:no-inherit
BeforeAll { Import-Module $PSScriptRoot/../../src/MyModule.psd1 }

The whole chain works in a sequential run and in a parallel run. In parallel the chain is
resolved once in the parent and the paths are handed to the workers, because a worker is a
separate runspace and cannot share the cache.

Run.RepoRoot is where the chain starts, and it is found for you by walking up from the
directory you are in until a .git folder shows up. That walk used to start from the process
working directory, which Set-Location does not change, so a session that started somewhere
else and then changed directory into a repository got a root pointing at the old place and
none of the setup files applied, with nothing to say why. It starts from the location the
session is actually in now. Set Run.RepoRoot yourself when your tests do not live in a git
repository, or when the root is somewhere other than where .git is.

Which files apply is a property of the directory, not of the container, so the run shares one
cache keyed by directory. Each directory is checked on disk once per run and each setup file
is tokenized once per run, however many test folders sit below them. On a tree with 60 test
folders and a 26 KB root setup file that is 23.6 ms instead of 845 ms.

A container reports which setup files applied to it, outermost first, in the order they ran:

U.Tests.ps1:
    <root>/Pester.BeforeContainer.ps1
    <root>/tests/Pester.BeforeContainer.ps1
    <root>/tests/unit/Pester.BeforeContainer.ps1
D.Tests.ps1:
    <root>/tests/docs/Pester.BeforeContainer.ps1

D.Tests.ps1 sits under a folder marked #pester:no-inherit, so only its own file applied. The
folder tree cannot tell you that, which is why the list is on the result.

Requested by @johlju in #2772, where he
estimated it removes around a thousand lines of duplication in SqlServerDsc.

See Behavior changes below, there are three.

Should-BeString shows you the whole diff

Comparing a long string used to give you a caret under the first character that differed.
That answers "one thing changed". Comparing a snapshot, a rendered template or a config file
is usually "many things changed", and a scan that stops at the first difference makes you fix
them one run at a time.

Should-BeString now finds every region that differs and prints them with context:

$expected = Get-Content ./expected/package.json -Raw
$actual   = Get-Content ./out/package.json -Raw
$actual | Should-BeString $expected
Expected strings to be the same, but they were different.
Expected length: 234
Actual length:   241
Expected 16 line(s), actual 16 line(s).
2 regions differ.

   1  1 |   {
   2  2 |     "name": "widget",
   3    | -   "version": "1.2.0",
      3 | +   "version": "1.3.0",
   4    | -   "license": "MIT",
      4 | +   "license": "Apache-2.0",
   5  5 |     "main": "index.js",
   6  6 |     "scripts": {
  ...
   9  9 |     },
  10 10 |     "dependencies": {
  11    | -     "left-pad": "^1.3.0"
     11 | +     "left-pad": "^1.3.1"
  12 12 |     },
  13 13 |     "engines": {
  14    | -     "node": ">=18"
     14 | +     "node": ">=20"
  15 15 |     }
  16 16 |   }

Expected line numbers on the left, actual line numbers on the right. They are separate columns
so it stays readable when lines are added or removed and the two sides stop lining up:

   1  1 |   Describe 'Api' {
      2 | +     BeforeAll {
      3 | +         Start-TestServer
      4 | +     }
      5 | + 
   2  6 |       It 'returns 200' {
  ...
   9    | -         $r.ContentType | Should-BeString 'application/json'
  10 13 |       }
  11 14 |   }

Only the lines that differ are expanded, so a tab or a trailing space is visible without
turning the surrounding context into escape codes.

Fixes #2951 and
[#3006](https:...

Read more

6.2.0-alpha1

6.2.0-alpha1 Pre-release
Pre-release

Choose a tag to compare

@nohwnd nohwnd released this 30 Aug 06:36
b6eaa72

Pester 6.2.0-alpha1

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

This is a prerelease. Install it with Install-Module Pester -AllowPrerelease.

Pester.BeforeContainer.ps1 grew from one file at the repository root into a chain that
follows your folder structure, so unit tests and integration tests can each have their own
setup without repeating it in every test file. Should-BeString prints a real diff when a
long string does not match. The configuration now tells you when you handed it a value it
cannot use, instead of quietly ignoring it. And the experimental parallel runner works on
Windows PowerShell 5.1, which is the slowest edition and the one that needed it most.

What's new?

Setup that follows your folders

6.1.0 shipped Pester.BeforeContainer.ps1, a setup file that runs before every test file,
including in parallel runs where each worker starts from a clean runspace. Only the one at
Run.RepoRoot was used though, so a repository where unit tests and integration tests need
different setup had to cram both into that single file, or repeat the setup in every test
file.

Now every Pester.BeforeContainer.ps1 from Run.RepoRoot down to the test file's own folder
is applied, outermost first:

reporoot/Pester.BeforeContainer.ps1                 <- applies to everything
reporoot/tests/Pester.BeforeContainer.ps1           <- applies to tests/ and below
reporoot/tests/unit/Pester.BeforeContainer.ps1      <- applies to tests/unit only
reporoot/tests/integration/Pester.BeforeContainer.ps1

The root file holds common setups and teardowns that every test needs:

# reporoot/Pester.BeforeContainer.ps1
BeforeAll {
    Import-Module $PSScriptRoot/src/MyModule.psd1 -Force
    . $PSScriptRoot/tests/helpers/Assertions.ps1
}

tests/ holds the helpers only tests use, and each suite holds what only it needs:

# reporoot/tests/Pester.BeforeContainer.ps1
BeforeAll {
    function New-TestUser { param($Name) [pscustomobject]@{ Name = $Name } }
}
# reporoot/tests/unit/Pester.BeforeContainer.ps1
BeforeAll { $script:Db = 'in-memory' }
# reporoot/tests/integration/Pester.BeforeContainer.ps1
BeforeAll { $script:Db = 'real-sql' }

A test file in tests/unit gets the root setup, the tests/ setup and the tests/unit
setup, in that order. A test file in tests/integration gets the root setup, the tests/
setup and real-sql. It does not get in-memory, whichever file happens to run first:

# reporoot/tests/unit/Get-User.Tests.ps1
Describe 'Get-User' {
    It 'uses the in-memory database' {
        $script:Db | Should-BeString 'in-memory'          # from tests/unit
        (New-TestUser -Name 'jakub').Name | Should-BeString 'jakub'   # from tests
    }
}

The setup files are dot-sourced into the container's own scope now, not into the run session
state. That is what makes the folders actually scope anything. Before this, setup dot-sourced
for one file stayed visible to every container after it, so a file in tests/integration
would silently inherit whatever tests/unit had set up, and the result depended on run order.

A folder opts out of everything above it with #pester:no-inherit, the same meaning
root = true has in an .editorconfig. Useful for something like doc tests that need their
own cheap setup and should not pay for the expensive one:

# reporoot/tests/docs/Pester.BeforeContainer.ps1
#pester:no-inherit
BeforeAll { Import-Module $PSScriptRoot/../../src/MyModule.psd1 }

The whole chain works in a sequential run and in a parallel run. In parallel the chain is
resolved once in the parent and the paths are handed to the workers, because a worker is a
separate runspace and cannot share the cache.

Run.RepoRoot is where the chain starts, and it is found for you by walking up from the
directory you are in until a .git folder shows up. That walk used to start from the process
working directory, which Set-Location does not change, so a session that started somewhere
else and then changed directory into a repository got a root pointing at the old place and
none of the setup files applied, with nothing to say why. It starts from the location the
session is actually in now. Set Run.RepoRoot yourself when your tests do not live in a git
repository, or when the root is somewhere other than where .git is.

Which files apply is a property of the directory, not of the container, so the run shares one
cache keyed by directory. Each directory is checked on disk once per run and each setup file
is tokenized once per run, however many test folders sit below them. On a tree with 60 test
folders and a 26 KB root setup file that is 23.6 ms instead of 845 ms.

Requested by @johlju in #2772, where he
estimated it removes around a thousand lines of duplication in SqlServerDsc.

See Behavior changes below, there are three.

Should-BeString shows you the whole diff

Comparing a long string used to give you a caret under the first character that differed.
That answers "one thing changed". Comparing a snapshot, a rendered template or a config file
is usually "many things changed", and a scan that stops at the first difference makes you fix
them one run at a time.

Should-BeString now finds every region that differs and prints them with context:

$expected = Get-Content ./expected/package.json -Raw
$actual   = Get-Content ./out/package.json -Raw
$actual | Should-BeString $expected
Expected strings to be the same, but they were different.
Expected length: 234
Actual length:   241
Expected 16 line(s), actual 16 line(s).
2 regions differ.

   1  1 |   {
   2  2 |     "name": "widget",
   3    | -   "version": "1.2.0",
      3 | +   "version": "1.3.0",
   4    | -   "license": "MIT",
      4 | +   "license": "Apache-2.0",
   5  5 |     "main": "index.js",
   6  6 |     "scripts": {
  ...
   9  9 |     },
  10 10 |     "dependencies": {
  11    | -     "left-pad": "^1.3.0"
     11 | +     "left-pad": "^1.3.1"
  12 12 |     },
  13 13 |     "engines": {
  14    | -     "node": ">=18"
     14 | +     "node": ">=20"
  15 15 |     }
  16 16 |   }

Expected line numbers on the left, actual line numbers on the right. They are separate columns
so it stays readable when lines are added or removed and the two sides stop lining up:

   1  1 |   Describe 'Api' {
      2 | +     BeforeAll {
      3 | +         Start-TestServer
      4 | +     }
      5 | + 
   2  6 |       It 'returns 200' {
  ...
   9    | -         $r.ContentType | Should-BeString 'application/json'
  10 13 |       }
  11 14 |   }

Only the lines that differ are expanded, so a tab or a trailing space is visible without
turning the surrounding context into escape codes.

Fixes #2951 and
#3006.

The configuration rejects values it cannot use

A configuration value of the wrong type used to be ignored, and a misspelled key was ignored
too, so the run just did not do what you asked and nothing said why. This bites hardest with
values that come out of a JSON or psd1 file, because those arrive as strings.

A value the option cannot use now throws while the configuration is built:

Invoke-Pester -Configuration @{ Run = @{ Parallel = 'yes' } }
# Cannot process argument transformation on parameter 'Configuration'. Cannot convert value
# "System.Collections.Hashtable" to type "PesterConfiguration". Error:
# "Run.Parallel expects a bool, but got the string 'yes'."

New-PesterConfiguration -Hashtable @{ Run = @{ Path =...
Read more

6.1.0

Choose a tag to compare

@nohwnd nohwnd released this 11 Aug 18:36
829890d

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

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 t...

Read more

6.1.0-rc1

6.1.0-rc1 Pre-release
Pre-release

Choose a tag to compare

@nohwnd nohwnd released this 06 Aug 20:37
ec8ca45

Pester 6.1.0-rc1

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

Pester 6.1.0 builds on the 6.0.0 release. The headline is that 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+.

This is a release candidate. The API is what we intend to ship as 6.1.0, but the features marked
experimental may still change based on your feedback.

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.

Sharper assertions

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

  • Should-BeString -NormalizeNewline 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" -NormalizeNewline   # 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.

  • 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.

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 pr...
Read more

5.9.1

Choose a tag to compare

@nohwnd nohwnd released this 11 Aug 18:35
81e59ad

5.9.1

A maintenance patch for Pester 5.

What's Changed

  • Fix #2953: the mock parameter filter no longer throws when a bound parameter's ToString throws (for example a mocked SMO type from New-MockObject) and debug messages are on, by @nohwnd in #2958
  • Build: roll the SDK forward across majors so rel/5.x.x builds on newer .NET SDKs, by @nohwnd in #2960

Full Changelog: 5.9.0...5.9.1

6.0.1

Choose a tag to compare

@nohwnd nohwnd released this 18 Jul 19:51
9213e2a

Pester 6.0.1

Fixes an intermittent crash in Run.Parallel on Windows PowerShell 7. When several test files imported Pester at the same time, the concurrent, unsynchronized writes to a shared process-global verb dictionary could corrupt it and throw Index was outside the bounds of the array, which killed the parallel run. The verb patch is now guarded by a lock. (#2901)

No other changes from 6.0.0.

6.1.0-alpha2

6.1.0-alpha2 Pre-release
Pre-release

Choose a tag to compare

@nohwnd nohwnd released this 17 Jul 06:24
3a3f9d5

What's Changed

  • Add experimental global mocks by @nohwnd in #2850

  • Allow empty string for Should-BeString -Expected by @nohwnd in #2863

  • Collect code coverage in parallel runs by @nohwnd in #2860

  • Render Describing/Context headers in parallel Detailed output by @nohwnd in #2853

  • Replay parallel worker debug output interleaved with tests by @nohwnd in #2854

  • Drop Run.BeforeContainer option, keep the Pester.BeforeContainer.ps1 convention by @nohwnd in #2859

  • Fix code coverage false negative for steppable-pipeline proxy functions by @nohwnd in #2871

  • Surface verbose/debug output when a CI debug flag is enabled by @nohwnd in #2870

Internal changes

Full Changelog: 6.1.0-alpha1...6.1.0-alpha2

6.1.0-alpha1

6.1.0-alpha1 Pre-release
Pre-release

Choose a tag to compare

@nohwnd nohwnd released this 09 Jul 12:12
9c752af

What's Changed

  • Add custom Should-* assertion authoring with New-ShouldAssertion by @nohwnd in #2848

Full Changelog: 6.0.0...6.1.0-alpha1

6.0.0

Choose a tag to compare

@nohwnd nohwnd released this 07 Jul 07:42
de3bc3d

Pester 6.0.0

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

Pester 6.0.0 builds on the v5 runtime (Discovery & Run, the configuration object, the rich result
object) and focuses on a brand new assertion syntax, faster code coverage, and an experimental
parallel runner.

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

What's new?

New Should-* assertions

The headline feature of Pester 6 is a completely new family of assertions. The
Assert project was merged into Pester and is now shipped as
first-class Should-* commands (note the dash, no space):

Describe 'Get-Planet' {
    It 'returns Earth' {
        Get-Planet | Should-Be 'Earth'
    }
}

Why a new syntax?

The classic Should -Be operator is flexible but loosely typed: -Be, -BeExactly, -Contain,
and friends all flow through one command, the left side is always unwrapped by the pipeline, and the
failure messages have to guess what you meant. The new assertions are specialized and
type-aware
, which means:

  • Clearer, more precise failure messages.
  • $Expected drives the comparison type, so 1 | Should-Be $true compares as booleans.
  • Specialized switches such as Should-BeString -IgnoreWhitespace live where they belong.
  • A consistent, predictable story for $null, empty collections, and single-item arrays.

See docs/assertion-types.md for the full design.

The four families

The assertions are grouped by how they treat $Actual and $Expected:

Family Examples Use for
Value – generic Should-Be, Should-NotBe, Should-BeGreaterThan, Should-BeSame, Should-BeNull, Should-HaveType A single value, compared like the PowerShell operators (Should-Be-eq).
Value – type specific Should-BeString, Should-MatchString/Should-BeLikeString, Should-BeTrue/Should-BeFalse, Should-BeFalsy/Should-BeTruthy, Should-BeBefore/Should-BeAfter, Should-BeFasterThan/Should-BeSlowerThan A value of a known type, with type-specific options.
Collection – generic Should-BeCollection, Should-ContainCollection, Should-NotContainCollection Comparing a whole collection item by item, or finding an ordered sub-collection within one.
Collection – combinator Should-All, Should-Any Asserting a condition across every / any item.

There are also dedicated assertions for exceptions (Should-Throw), mocks (Should-Invoke,
Should-NotInvoke), command metadata (Should-HaveParameter), hashtable shape
(Should-BeHashtable), and deep object comparison (Should-BeEquivalent).

Pipeline vs. -Actual

The actual value can be provided by the pipeline or by the -Actual parameter:

1 | Should-Be -Expected 1
Should-Be -Actual 1 -Expected 1

The pipeline unwraps its input, so a value assertion treats 1 and @(1) the same, and @()
as $null. A collection assertion treats the same input as @(1) and @(). When you need to
preserve the exact value or the concrete collection type (for example [int[]]), use -Actual,
which passes the value through unchanged:

# Value assertion – these all pass:
1     | Should-Be -Expected 1
@(1)  | Should-Be -Expected 1
$null | Should-Be -Expected $null

# Collection assertion:
1, 2, 3 | Should-BeCollection @(1, 2, 3)
@()     | Should-BeCollection @()

# -Actual preserves the original type:
Should-HaveType -Actual ([int[]](1, 2)) -Expected ([int[]])

If you forget and pipe a collection into a value or type assertion, the unwrapping is never a silent
surprise: when it makes the assertion fail, the message adds a hint that explains what the pipeline
did and points you back to -Actual.

[int[]](1, 2) | Should-HaveType ([int[]])
# Expected value to have type [int[]], but got [Object[]] @(1, 2).
#
# Hint: You piped a [int[]] into a type assertion, but the pipeline streams a multi-item
# collection and re-collects it as [Object[]], so the assertion saw [Object[]], not the
# [int[]] you piped. To assert the type of a collection, pass it as the -Actual argument
# instead of piping it, e.g. -Actual $value.

More examples

# Strings, with type-specific options
'  hello ' | Should-BeString 'hello' -TrimWhitespace
'Hello'    | Should-BeString 'hello' -CaseSensitive   # fails, shows a diff with an arrow marker

# Booleans and null
$true              | Should-BeTrue
(Get-Item .)       | Should-NotBeNull
$result.Error      | Should-BeNull

# Collections
1, 2, 3            | Should-BeCollection @(1, 2, 3)
1, 2, 3            | Should-BeCollection -Count 3
1, 2, 3            | Should-All { $_ -gt 0 }
1, 2, 3            | Should-Any { $_ -gt 2 }
@('a', 'b', 'c')   | Should-ContainCollection @('a', 'c')   # ordered sub-collection, gaps allowed

# Exceptions
{ throw 'kaboom' } | Should-Throw -ExceptionMessage 'kaboom'
{ throw [System.InvalidOperationException]::new('nope') } |
    Should-Throw -ExceptionType ([System.InvalidOperationException])

# Time
{ Start-Sleep -Milliseconds 10 }  | Should-BeFasterThan '100ms'
[datetime]::Now.AddMinutes(11)    | Should-BeAfter 10minutes -Ago
[datetime]::Now.AddMinutes(20)    | Should-BeAfter -Now

# Command metadata
Get-Command Get-Process | Should-HaveParameter -ParameterName Name -Type ([string[]])

Soft assertions

Like Should, the new assertions honor Should.ErrorAction = 'Continue', so you can collect
multiple failures from a single It instead of stopping at the first one:

$config = New-PesterConfiguration
$config.Should.ErrorAction = 'Continue'

Describe 'user' {
    It 'has the expected shape' {
        $user.Name | Should-Be 'Jakub'
        $user.Age  | Should-Be 31
        $user.City | Should-Be 'Prague'
    }
}

All three assertions run, and every failure is reported at the end of the test.

Choosing your assertion syntax

The classic Should -Be assertions still ship and still work — the new Should-* assertions are
additive. You can adopt them gradually, or mix both in the same suite while you migrate.

When you are ready to commit to the new style, you can switch the old syntax off so it can't be used
by accident:

$config = New-PesterConfiguration
$config.Should.DisableV5 = $true   # using `Should -Be` now throws

Deep object comparison with Should-BeEquivalent

Most assertions compare a single value. Should-BeEquivalent is different: it does a deep,
recursive
comparison of two objects, walking nested properties, hashtables, dictionaries, and
collections. It is the right tool for asserting on a whole API response, a configuration object, or
any rich structure in one shot — and it produces a readable, property-by-property diff when the two
sides don't match.

$user = Get-User
$user | Should-BeEquivalent ([pscustomobject]@{
    Name    = 'Jakub'
    Age     = 31
    Address = [pscustomobject]@{ City = 'Prague'; Country = 'CZ' }
    Roles   = @('admin', 'user')
})

By default the comparison is strict and symmetric: every member on both sides has to match, so an
unexpected extra property on the actual object fails the assertion. Compare like with like — an
object to an object, a hashtable to a hashtable. The two options below relax that strictness in the
ways you'll most often want.

Compare only what's on the expected object — -ExcludePathsNotOnExpected

Reach for this when the actual object is large and you only care about a few fields. Any property
that is not present on the expected object is ignored entirely, so you can assert a subset of
a big object without spelling out everything you don't care about:

$user = Get-User   # Name, Age, Id, CreatedAt, LastLogin, PasswordHash, ...

# Passes as long as Name and Age match. The other properties are never even looked at.
$user | Should-BeEquivalent ([pscustomobject]@{ Name = 'Jakub'; Age = 31 }) -ExcludePathsNotOnExpected

This keeps tests focused and resilient: adding a new field to the object under test won't break an
assertion that never claimed to care about it.

Ignore specific paths — -ExcludePath

When you want a full comparison except for a few volatile fields (generated ids, timestamps),
exclude them by name. Use dot-notation to reach nested members:

$actual | Should-BeEquivalent $expected -ExcludePath 'Id', '...
Read more