github pester/Pester 5.0.0

latest releases: 6.0.0-alpha5, 6.0.0-alpha4, 6.0.0-alpha3...
4 years ago

Pester 5.0.0

💵 I am spending most of my weekends making this happen. These release notes for example took multiple days to write and update. Consider sponsoring me or sponsoring Pester, please.

🙋‍ Want to share feedback? Go here, or see more options in Questions?.

  • Pester 5.0.0
    • What is new?
      • Discovery & Run
        • Put setup in BeforeAll
        • Review your usage of Skip
        • Review your usage of TestCases
      • Tags
        • Tags on everything
        • Tags use wildcards
      • Logging
      • Run only what is needed
      • Skip on everything
      • Collect all Should failures
      • Collecting AfterEach failures
      • Normal, Detailed and Diagnostic view
      • New result object
      • Simple and advanced interface
        • Simple interface
        • Advanced interface
      • PesterPreference
      • Scoping of BeforeAll & AfterAll
      • Scoping of BeforeEach & AfterEach
      • Implicit parameters for TestCases
      • Mocking
        • Mocks are scoped based on their placement
        • Counting mocks depends on placement
        • Should -Invoke
        • Default parameters for ParameterFilter
        • Mocks can be debugged
        • Internal Mock functions are hidden
        • Avoid putting in InModuleScope around your Describe and It blocks
      • VSCode improvements
        • Use legacy code lens
        • Output verbosity
    • Breaking changes
      • Actual breaking changes
      • Deprecated features
      • Additional issues to be solved in 5.1
  • Questions?

What is new?

🔥 Interested only in breaking changes? See breaking changes below.

🕹 want to see a demo? Here is my talk What is new in Pester 5 from #BridgeConf

Pester 5.0.0 is finally here! 🥳 This version recommended to be used for new projects, and is the recommended choice if you just started learning Pester. If you own any project, please give it a try and report back to help me identify bugs.

Discovery & Run

The fundamental change in this release is that Pester now runs in two phases: Discovery and Run. During discovery, it quickly scans your test files and discovers all the Describes, Contexts, Its and other Pester blocks.

This powers many of the features in this release and enables many others to be implemented in the future.

To reap the benefits, there are new rules to follow:

Put all your code into It, BeforeAll, BeforeEach, AfterAll or AfterEach. Put no code directly into Describe, Context or on the top of your file, without wrapping it in one of these blocks, unless you have a good reason to do so.

All misplaced code will run during Discovery, and its results won't be available during Run.

This will allow Pester to control when all of your code is executed, and scope it correctly. This will also keep the amount of code executed during discovery to a minimum. Keeping it fast and responsive. See discovery and script setup article for detailed information.

Put setup in BeforeAll

If your test suite already puts its setups and teardowns into Before* and After*. All you need to do is move the file setup into a BeforeAll block:

BeforeAll {
    # DON'T use $MyInvocation.MyCommand.Path
    . $PSCommandPath.Replace('.Tests.ps1','.ps1')
}

Describe "Get-Cactus" {
    It "Returns 🌵" {
        Get-Cactus | Should -Be '🌵'
    }
}

See migration script for a script that does it for you. Improvements are welcome, e.g. putting code between Describe and It into BeforeAll. See discovery and script setup and importing ps files article for detailed information.

Review your usage of Skip

This also impacts -Skip when you use it with -Skip:$SomeCondition. All the code in the describe block, including your skip conditions and TestCases will be evaluated during Discovery. Prefer static global variables, or code that is cheap to executed. It is not forbidden to put code to figure out the skip outside of BeforeAll, but be aware that it will run on every discovery.

This won't work. BeforeAll runs after Discovery, and so $isSkipped is not defined and ends up being $null -> $false, so the test will run.

Describe "d" {
    BeforeAll {
        function Get-IsSkipped {
            Start-Sleep -Second 1
            $true
        }
        $isSkipped = Get-IsSkipped
    }

    It "i" -Skip:$isSkipped {

    }
}

Changing the code like this will skip the test correctly, but be aware that the code will run every time Discovery is performed on that file. Depending on how you run your tests this might be every time.

function Get-IsSkipped {
    Start-Sleep -Second 1
    $true
}
$isSkipped = Get-IsSkipped

Describe "d" {
    It "i" -Skip:$isSkipped {

    }
}

Consider settings the check statically into a global read-only variable (much like $IsWindows), or caching the response for a while. Are you in this situation? Get in touch via the channels mentioned in Questions?.

Review your usage of TestCases

-TestCases, much like -Skip are evaluated during discovery and saved for later use when the test runs. This means that doing expensive setup for them will be happening every Discovery. On the other hand, you will now find their complete content for each TestCase in Data on the result test object. And also don't need to specify param block.

Tags

Tags on everything

The tag parameter is now available on Describe, Context and It and it is possible to filter tags on any level. You can then use -Tag and -ExcludeTag to run just the tests that you want.

Here you can see an example of a test suite that has acceptance tests and unit tests, and some of the tests are slow, some are flaky, and some only work on Linux. Pester5 makes running all reliable acceptance tests, that can run on Windows is as simple as:

Invoke-Pester $path -Tag "Acceptance" -ExcludeTag "Flaky", "Slow", "LinuxOnly"
Describe "Get-Beer" {

    Context "acceptance tests" -Tag "Acceptance" {

        It "acceptance test 1" -Tag "Slow", "Flaky" {
            1 | Should -Be 1
        }

        It "acceptance test 2" {
            1 | Should -Be 1
        }

        It "acceptance test 3" -Tag "WindowsOnly" {
            1 | Should -Be 1
        }

        It "acceptance test 4" -Tag "Slow" {
            1 | Should -Be 1
        }

        It "acceptance test 5" -Tag "LinuxOnly" {
            1 | Should -Be 1
        }
    }

    Context "unit tests" {

        It "unit test 1" {
            1 | Should -Be 1
        }

        It "unit test 2" -Tag "LinuxOnly" {
            1 | Should -Be 1
        }

    }
}
Starting test discovery in 1 files.
Discovering tests in ...\real-life-tagging-scenarios.tests.ps1.
Found 7 tests. 482ms
Test discovery finished. 800ms

Running tests from '...\real-life-tagging-scenarios.tests.ps1'
Describing Get-Beer
  Context acceptance tests
      [+] acceptance test 2 50ms (29ms|20ms)
      [+] acceptance test 3 42ms (19ms|23ms)
Tests completed in 1.09s
Tests Passed: 2, Failed: 0, Skipped: 0, Total: 7, NotRun: 5

Tags use wildcards

The tags are now also compared as -like wildcards, so you don't have to spell out the whole tag if you can't remember it. This is especially useful when you are running tests locally:

Invoke-Pester $path -ExcludeT "Accept*", "*nuxonly" | Out-Null
Starting test discovery in 1 files.
Discovering tests in ...\real-life-tagging-scenarios.tests.ps1.
Found 7 tests. 59ms
Test discovery finished. 97ms


Running tests from '...\real-life-tagging-scenarios.tests.ps1'
Describing Get-Beer
 Context Unit tests
   [+] unit test 1 15ms (7ms|8ms)
Tests completed in 269ms
Tests Passed: 1, Failed: 0, Skipped: 0, Total: 7, NotRun: 6

Logging

All the major components log extensively.I am using logs as a debugging tool all the time so I make sure the logs are usable and not overly verbose. See if you can figure out why acceptance test 1 is excluded from the run, and why acceptance test 2 runs.

Filter: (Get-Beer) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer) Block did not match the exclude tag filter, moving on to the next filter.
Filter: (Get-Beer) There is 'Acceptance' include tag filter.
Filter: (Get-Beer) Block has no tags, moving to next include filter.
Filter: (Get-Beer) Block did not match any of the include filters, but it will still be included in the run, it's children will determine if it will run.
Filter: (Get-Beer.acceptance tests) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.acceptance tests) Block did not match the exclude tag filter, moving on to the next filter.
Filter: (Get-Beer.acceptance tests) There is 'Acceptance' include tag filter.
Filter: (Get-Beer.acceptance tests) Block is included, because it's tag 'Acceptance' matches tag filter 'Acceptance'.
Filter: (Get-Beer.acceptance tests.acceptance test 1) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.acceptance tests.acceptance test 1) Test is excluded, because it's tag 'Flaky' matches exclude tag filter 'Flaky'.
Filter: (Get-Beer.acceptance tests.acceptance test 2) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.acceptance tests.acceptance test 2) Test did not match the exclude tag filter, moving on to the next filter.
Filter: (Get-Beer.acceptance tests.acceptance test 2) Test is included, because its parent is included.
Filter: (Get-Beer.acceptance tests.acceptance test 3) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.acceptance tests.acceptance test 3) Test did not match the exclude tag filter, moving on to the next filter.
Filter: (Get-Beer.acceptance tests.acceptance test 3) Test is included, because its parent is included.
Filter: (Get-Beer.acceptance tests.acceptance test 4) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.acceptance tests.acceptance test 4) Test is excluded, because it's tag 'Slow' matches exclude tag filter 'Slow'.
Filter: (Get-Beer.acceptance tests.acceptance test 5) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.acceptance tests.acceptance test 5) Test is excluded, because it's tag 'LinuxOnly' matches exclude tag filter 'LinuxOnly'.
Filter: (Get-Beer.Unit tests) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.Unit tests) Block did not match the exclude tag filter, moving on to the next filter.
Filter: (Get-Beer.Unit tests) There is 'Acceptance' include tag filter.
Filter: (Get-Beer.Unit tests) Block has no tags, moving to next include filter.
Filter: (Get-Beer.Unit tests) Block did not match any of the include filters, but it will still be included in the run, it's children will determine if it will run.
Filter: (Get-Beer.Unit tests.unit test 1) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.Unit tests.unit test 1) Test did not match the exclude tag filter, moving on to the next filter.
Filter: (Get-Beer.Unit tests.unit test 1) There is 'Acceptance' include tag filter.
Filter: (Get-Beer.Unit tests.unit test 1) Test has no tags, moving to next include filter.
Filter: (Get-Beer.Unit tests.unit test 1) Test did not match any of the include filters, it will not be included in the run.
Filter: (Get-Beer.Unit tests.unit test 2) There is 'Flaky, Slow, LinuxOnly' exclude tag filter.
Filter: (Get-Beer.Unit tests.unit test 2) Test is excluded, because it's tag 'LinuxOnly' matches exclude tag filter 'LinuxOnly'.
Filter: (Get-Beer.Unit tests) Block was marked as Should run based on filters, but none of its tests or tests in children blocks were marked as should run. So the block won't run.

Please be aware that the log is currently only written to the screen and not persisted in the result object. And that the logging comes with a performance penalty. You can enable logging by using the configuration object, or by using -Output Diagnostic.

Run only what is needed

Look at the last line of the above log. It says that the block will not run, because none of the tests inside of it, or inside of any of the children blocks will run. This is great because when the block does not run, none of its setups and teardowns run either.

Invoking the code below with -ExcludeTag Acceptance will filter out all the tests in the file and there will be nothing to run. Pester5 understands that if there are no tests in the file to run, there is no point in executing the setups and teardowns in it, and so it returns almost immediately:

BeforeAll {
    Start-Sleep -Seconds 3
}

Describe "describe 1" {
    BeforeAll {
        Start-Sleep -Seconds 3
    }

    It "acceptance test 1" -Tag "Acceptance" {
        1 | Should -Be 1
    }

    AfterAll {
        Start-Sleep -Seconds 3
    }
}
Starting test discovery in 1 files.
Found 1 tests. 64ms
Test discovery finished. 158ms
Tests completed in 139ms
Tests Passed: 0, Failed: 0, Skipped: 0, Total: 1, NotRun: 1

Skip on everything

-Skip is now available on Describe and Context. This allows you to skip all the tests in that block and every child block.

Describe "describe1" {
    Context "with one skipped test" {
        It "test 1" -Skip {
            1 | Should -Be 2
        }

        It "test 2" {
            1 | Should -Be 1
        }
    }

    Describe "that is skipped" -Skip {
        It "test 3" {
            1 | Should -Be 2
        }
    }

    Context "that is skipped and has skipped test" -Skip {
        It "test 3" -Skip {
            1 | Should -Be 2
        }

        It "test 3" {
            1 | Should -Be 2
        }
    }
}
Starting test discovery in 1 files.
Found 5 tests. 117ms
Test discovery finished. 418ms
Describing describe1
 Context with one skipped test
   [!] test 1, is skipped 18ms (0ms|18ms)
   [+] test 2 52ms (29ms|22ms)
 Describing that is skipped
   [!] test 3, is skipped 12ms (0ms|12ms)
 Context that is skipped and has skipped test
   [!] test 3, is skipped 10ms (0ms|10ms)
   [!] test 3, is skipped 10ms (0ms|10ms)
Tests completed in 1.03s
Tests Passed: 1, Failed: 0, Skipped: 4, Total: 5, NotRun: 0

(Pending is translated to skipped, Inconclusive does not exist anymore. Are you relying on them extensively? Share your feedback.)

Collect all Should failures

Should can now be configured to continue on failure. This will report the error to Pester, but won't fail the test immediately. Instead, all the Should failures are collected and reported at the end of the test. This allows you to put multiple assertions into one It and still get complete information on failure.

function Get-User {
    @{
        Name = "Jakub"
        Age = 31
    }
}

Describe "describe" {
    It "user" {
        $user = Get-User

        $user | Should -Not -BeNullOrEmpty -ErrorAction Stop
        $user.Name | Should -Be "Tomas"
        $user.Age | Should -Be 27

    }
}
Starting test discovery in 1 files.
Found 1 tests. 51ms
Test discovery finished. 83ms
Describing describe
  [-] user 124ms (109ms|15ms)
   [0] Expected strings to be the same, but they were different.
   String lengths are both 5.
   Strings differ at index 0.
   Expected: 'Tomas'
   But was:  'Jakub'
   at $user.Name | Should -Be "Tomas"
   [1] Expected 27, but got 31.
   at $user.Age | Should -Be 27
Tests completed in 286ms
Tests Passed: 0, Failed: 1, Skipped: 0, Total: 1, NotRun: 0

This allows you to check complex objects easily without writing It for each of the properties that you want to test. You can also use -ErrotAction Stop to force a failure when a pre-condition is not met. In our case if $user was null, there would be no point in testing the object further and we would fail the test immediately.

This new Should behavior is opt-in and can be enabled via Should.ErrorAction = 'Continue' on the configuration object or via $PesterPreference more on that below.

Collecting AfterEach failures

In a similar fashion to Should, when test assertion fails, and it teardown also fails you will see both errors:

[-] fails the test 30ms (24ms|5ms)
 [0] Expected 2, but got 1.
 at 1 | Should -Be 2
 [1] RuntimeException: but also fails in after each

Normal, Detailed and Diagnostic view

Errors are usually what we are interested in when running tests. And that is why Pester5 implements a concise view that prints failed tests with the full test path, and minimal discovery and summary information:

Starting test discovery in 1 files.
Test discovery finished. 83ms
[-] output.fails 24ms (18ms|5ms)
 Expected 2, but got 1.
 at 1 | Should -Be 2, C:\Projects\pester\Pester.RSpec.Demo.ts.ps1:289
 at <ScriptBlock>, C:\Projects\pester\Pester.RSpec.Demo.ts.ps1:289
[-] output.child.fails 22ms (16ms|5ms)
 Expected 2, but got 1.
 at 1 | Should -Be 2, C:\Projects\pester\Pester.RSpec.Demo.ts.ps1:298
 at <ScriptBlock>, C:\Projects\pester\Pester.RSpec.Demo.ts.ps1:298
Tests completed in 331ms
Tests Passed: 4, Failed: 2, Skipped: 0, Total: 6, NotRun: 0

Pester5 also implements a Diagnostic view that prints information from Discovery, Filter, Skip, Mock and other sources, this output will be by default enabled when debugging tests in VSCode, or you can enable it by using -Output Diagnostic.

Mock: Found 2 behaviors for 'Get-Emoji':
    Body: { '🚒' }
    Filter: { $Emoji -eq 'firetruck' }
    Verifiable: False
    Body: { '🔥' }
    Filter: $null
    Verifiable: False
Mock: We are in a test. Returning mock table from test scope.
Mock: Finding a mock behavior.
Mock: Running mock filter {  $Emoji -eq 'firetruck'  } with context: Emoji = firetruck.
Mock: Mock filter passed.
Mock: {  '🚒'  } passed parameter filter and will be used for the mock call.
Mock: Executing mock behavior for mock Get-Emoji.
Mock: Behavior for Get-Emoji was executed.
Mock: Removing function PesterMock_3b8fd8ae-de6a-4a1c-87a8-7b177071f4af and aliases Get-Emoji for Get-Emoji.
  [+] Gets firetruck 2.78s (2.71s|64ms)
Tests completed in 4.71s
Tests Passed: 1, Failed: 0, Skipped: 0 NotRun: 0

New result object

The new result object is extremely rich, and used by Pester internally to make all of its decisions. Most of the information in the tree is unprocessed to allow you to to work with the raw data. You are welcome to inspect the object, and write your code based on it.

To use your current CI pipeline with the new object use ConvertTo-Pester4Result to convert it. To convert the new object to NUnit report use ConvertTo-NUnitReport or specify the -CI switch to enable NUnit output, code coverage and exit code on failure.

Simple and advanced interface

Invoke-Pester is extremely bloated in Pester4. Some of the parameters consume hashtables that I always have to google, and some of the names don't make sense anymore. In Pester5 I aimed to simplify this interface and get rid of the hashtables. Right now I landed on two vastly different apis. With a big hole in the middle that still remains to be defined. There is the Simple interface that looks like this:

Invoke-Pester -Path <String[]>
              -ExcludePath <String[]>
              -Tag <String[]>
              -ExcludeTag <String[]>
              -Output <String>
              -CI

And the Advanced interface that takes just Pester configuration object and nothing else:

Invoke-Pester -Configuration <PesterConfiguration>

A mapping of the parameters of the simple interface to the configuration object properties on the advanced interface is:

Parameter Configuration Object Property
Path Run.Path
ExcludePath Run.ExcludePath
Tag Filter.Tag
ExcludeTag Filter.ExcludeTag
Output Output.Verbosity
CI CodeCoverage.Enabled, TestResult.Enabled and Run.Exit (all set to $true)

Simple interface

The simple interface is what I mostly need to run my tests. It uses some sensible defaults, and most of the parameters are hopefully self explanatory. The CI switch enables NUnit output to testResults.xml, code coverage that is automatically figured out from the provided files and exported into coverage.xml and also enables exit with error code when anything fails.

Advanced interface

Advanced interface uses PesterConfiguration object which contains all options that you can provide to Pester and contains descriptions for all the configuration sections and as well as default values. Here is what you see when you look at the default Debug section of the object:

[PesterConfiguration]::Default.Debug | Format-List

ShowFullErrors         : Show full errors including Pester internal stack. (False, default: False)
WriteDebugMessages     : Write Debug messages to screen. (False, default: False)
WriteDebugMessagesFrom : Write Debug messages from a given source, WriteDebugMessages must be set to true for this to work. You can use like wildcards to get messages from multiple sources, as well as * to get everything. (*, default: *)
ShowNavigationMarkers  : Write paths after every block and test, for easy navigation in VSCode. (False, default: False)
WriteVSCodeMarker      : Write VSCode marker for better integration with VSCode. (False, default: False)

The configuration object can be constructed either via the Default static property or by casting a hashtable to it. You can also cast a hashtable to any of its sections. Here are three different ways to the same goal:

# get default from static property
$configuration = [PesterConfiguration]::Default
# assing properties & discover via intellisense
$configuration.Run.Path = 'C:\projects\tst'
$configuration.Filter.Tag = 'Acceptance'
$configuration.Filter.ExcludeTag = 'WindowsOnly'
$configuration.Should.ErrorAction = 'Continue'
$configuration.CodeCoverage.Enabled = $true

# cast whole hashtable to configuration
$configuration = [PesterConfiguration]@{
    Run = @{
        Path = 'C:\projects\tst'
    }
    Filter = @{
        Tag = 'Acceptance'
        ExcludeTag = 'WindowsOnly'
    }
    Should = @{
        ErrorAction = 'Continue'
    }
    CodeCoverage = @{
        Enable = $true
    }
}

# cast from empty hashtable to get default
$configuration = [PesterConfiguration]@{}
$configuration.Run.Path = 'C:\projects\tst'
# cast hashtable to section
$configuration.Filter = @{
        Tag = 'Acceptance'
        ExcludeTag = 'WindowsOnly'
    }
$configuration.Should.ErrorAction = 'Continue'
$configuration.CodeCoverage.Enabled = $true

This configuration object contains all the options that are currently supported and the Simple interface is internally translates to this object internally. It is the source of truth for the defaults and configuration. The Intermediate api will be figured out later, as well as all the other details.

PesterPreference

There is one more way to provide the configuration object which is $PesterPreference. On Invoke-Pester (in case of interactive execution Invoke-Pester is called inside of the first Describe) the preference is collected and merged with the configuration object if provided. This allows you to configure everything that you would via Invoke-Pester also when you are running interactively (via F5). You can also use this to define the defaults for your session by putting $PesterPreference into your PowerShell profile.

Here is a simple example of enabling Mock logging output while running interactively :

$PesterPreference = [PesterConfiguration]::Default
$PesterPreference.Debug.WriteDebugMessages = $true
$PesterPreference.Debug.WriteDebugMessagesFrom = "Mock"

BeforeAll {
    function a { "hello" }
}
Describe "pester preference" {
    It "mocks" {
        Mock a { "mock" }
        a | Should -Be "mock"
    }
}
Starting test discovery in 1 files.
Discovering tests in C:\Users\jajares\Desktop\mck.tests.ps1.
Found 1 tests. 44ms
Test discovery finished. 80ms


Running tests from 'C:\Users\jajares\Desktop\mck.tests.ps1'
Describing pester preference
Mock: Setting up mock for a.
Mock: We are in a test. Returning mock table from test scope.
Mock: Resolving command a.
Mock: Searching for command  in the caller scope.
Mock: Found the command a in the caller scope.
Mock: Mock does not have a hook yet, creating a new one.
Mock: Defined new hook with bootstrap function PesterMock_b0bde5ee-1b4f-4b8f-b1dd-aef38b3bc13d and aliases a.
Mock: Adding a new default behavior to a.
Mock: Mock bootstrap function a called from block Begin.
Mock: Capturing arguments of the mocked command.
Mock: Mock for a was invoked from block Begin.
Mock: Getting all defined mock behaviors in this and parent scopes for command a.
Mock: We are in a test. Finding all behaviors in this test.
Mock: Found behaviors for 'a' in the test.
Mock: Finding all behaviors in this block and parents.
... shortened Mock does a lot of stuff
Verifiable: False
Mock: We are in a test. Returning mock table from test scope.
Mock: Removing function PesterMock_b0bde5ee-1b4f-4b8f-b1dd-aef38b3bc13d and aliases a for .
  [+] mocks 857ms (840ms|16ms)
Tests completed in 1.12s
Tests Passed: 1, Failed: 0, Skipped: 0, Total: 1, NotRun: 0

Scoping of BeforeAll & AfterAll

BeforeAll can now be placed above the the top-level Describe, and you should move your setup into it. AfterAll is not allowed in the top level block.

The scoping is very similar to Pester v4, but without the quirks. BeforeAll, Describe, and AfterAll now run all in the same scope, and are isolated from their parent scope, to avoid leaking variables outside of their scopes, for example into the script scope. This prevents test cross-pollution and makes your tests more repeatable.

Failures in setup and teardown work very similar to how they worked in Pester v4, a failure in a Describe block, BeforeAll or AfterAll will fail the whole block. The nice side-effect of having test discovery is that we now know how many tests were in that failed block and can report all tests that were supposed to run as failed.

Scoping of BeforeEach & AfterEach

BeforeEach, It and AfterEach now run in the same scope, but are still isolated from their parent to avoid leaking variables and test cross-pollution.

Running in single scope allows you to take a portion of your It and move it into BeforeEach without any change in behavior. And it also allows you to change a variable in It, and use the updated value in AfterAll.

Implicit parameters for TestCases

Test cases are super useful, but I find it a bit annoying, and error prone to define the param block all the time, so when invoking It I am defining the variables in parent scope, and also splatting them. As a result you don't have to define the param block:

Describe "a" {
    It "b" -TestCases @(
        @{ Name = "Jakub"; Age = 30 }
    ) {
        $Name | Should -Be "Jakub"
    }
}

This also works for mock

Mocking

Mocks are scoped based on their placement

Mocks are no longer effective in the whole Describe / Context in which they were placed. Instead they will defualt to the block in which they were placed. Both of these work:

Describe "d" {
    BeforeAll {
        function f () { "real" }
    }

    It "i" {
        Mock f { "mock" }
        f | Should -Be "mock"
    }

    It "j" {
        f | Should -Be "real"
    }
}

Describe "d" {
    BeforeAll {
        function f () { "real" }
        Mock f { "mock" }
    }

    It "i" {
        f | Should -Be "mock"
    }

    It "j" {
        f | Should -Be "mock"
    }
}

Counting mocks depends on placement

Counting mocks depends on where the assertion is placed. In It, BeforeEach and AfterEach it defaults to It scope. In Describe, Context, BeforeAll and AfterAll, it default to Describe or Context based on the command that contains them. The default can still be overriden by specifying -Scope parameter.

Describe "d" {

    BeforeAll {
        function f () { "real" }
        Mock f { "mock" }
    }

    It "i" {
        f
        Should -Invoke f -Exactly 1
    }

    It "j" {
        f
        Should -Invoke f -Exactly 1
    }

    It "k" {
        f
        Should -Invoke f -Exactly 3 -Scope Describe
    }

    AfterEach {
        Should -Invoke f -Exactly 1
    }

    AfterAll {
        Should -Invoke f -Exactly 3
    }
}

Should -Invoke

Mock counting assertions were renamed to Should -Invoke and Should -InvokeVerifiable, and most of their parameters are no longer positional. Assert-MockCalled and Assert-VerifiableMock are provided as functions, and are deprecated.

Default parameters for ParameterFilter

Parameter filters no longer require you to use param().

Describe "d" {

    BeforeAll {
        function f ($a) { "real" }
        Mock f { "mock" } -ParameterFilter { $a -eq 10 }
    }

    It "i" {
        f 10
        Should -Invoke f -Exactly 1
    }

    It "j" {
        f 20
        Should -Invoke f -Exactly 0
    }
}

Mocks can be debugged

Mocks don't rewrite the scriptblock you provide anymore. You can now set breakpoints into them as well as any of the ParameterFilter or Should -Invoke Parameter filter.

Internal Mock functions are hidden

v4 published few internal functions that were needed to successfully call back into Pester from the mock bootstrap function. In v5 I came up with a little trick that enabled me to remove all the internal functions from the public API.

When Pester generates the mock bootstrap function it produces a command info object (the thing you get from Get-Command <some command>). I take that object and attach a new property on it that contains data from Pester. When the bootstrap function executes, it can simply use $MyInvocation.MyCommand to reach the same command info object, and so it can reach the data Pester gave it. Among this data is a command info of internal Pester function Invoke-Mock which is then simply invoked by &.

Avoid putting in InModuleScope around your Describe and It blocks

InModuleScope is a simple way to expose your internal module functions to be tested, but it prevents you from properly testing your published functions, does not ensure that your functions are actually published, and slows down Discovery by loading the module. Aim to avoid it altogether by using -ModuleName on Mock. Or at least avoid placing InModuleScope outside of It.

VSCode improvements

Use legacy code lens

With Pester 5 it is finally possible to run and debug just a single test in VSCode!

Shows a single test being run using the new Code Lense

In the latest PowerShell Preview extension for VSCode you can enable Use Legacy Code Lens option which will enable Run tests on all Describe, Context and It blocks. You can run a whole block, any child block, or any test individually. You can also run tests that are marked as skipped by running them individually.

Actually there is a bug, and the option is called Enable Legacy Code Lens, and is enabled by default and can't be disabled. 😁 Take advantage of this and go try it right now!

Output verbosity

You can specify verbosity in VSCode, to see normal or detailed output, or to take it from PesterPreference. This also works for Pester 4!

Shows a run with minimal preference that shows only errors

Breaking changes

Actual breaking changes

  • PowerShell 2 is no longer supported
  • Documentation is out of date for all commands
  • Legacy syntax Should Be (without -) is removed, see Migrating from Pester v3 to v4
  • Mocks are scoped based on their placement, not in whole Describe / Context. The count also depends on their placement. See mock scoping
  • Assert-VerifiableMocks was removed, see Should -Invoke
  • The api changed significantly, and the intermediate api is not present in this release. See simple and advanced interface above on how to invoke Pester.
  • All code placed in the body of Describe outside of It, BeforeAll, BeforeEach, AfterAll, AfterEach will run during discovery and it's state might or might not be available to the test code, see basics of discovery
  • -Output parameter has reduced options to None, Normal, Detailed and Diagnostic, -Show alias is removed
  • -PesterOption switch is removed
  • -TestName switch is replaced with -FullNameFilter switch
  • -Script option was renamed to -Path and takes paths only, it does not take hashtables. Parametrized scripts are not implemented at the moment, which should be solved in 5.1
  • Using $MyInvocation.MyCommand.Path to locate your script in BeforeAll does not work. This does not break it for your scripts and modules. Use $PSScriptRoot or $PSCommandPath. See importing ps files article for detailed information.
  • Should -Throw is using -like to match the exception message instead of .Contains. Use * or any of the other -like wildcard to match only part of the message.
  • Variables defined during Discovery, are not available in Before*, After* and It. When generating tests via foreach blocks, make sure you pass all variables into the test using -TestCases.

Deprecated features

  • Assert-MockCalled is deprecated, it is recommended to use Should -Invoke
  • Assert-VerifiableMock is deprecated, it is recommended to use Should -InvokeVerifiable

Additional issues to be solved in 5.1

  • -Strict switch is not available

  • Inconclusive and Pending states are currently no longer available, -Pending and -Inconclusive are translated to -Skip both on test blocks and when using Set-ItResult

  • Code coverage report is not available.

  • Automatic Code coverage via -CI switch is largely untested.

  • Generating tests during using foreach during discovery time works mostly, generating them from BeforeAll, to postpone expensive work till it is needed in case the test is filtered out also works, but is hacky. Get in touch if you need it and help me refine it.

  • Running on huge codebases is largely untested

  • IncludeVSCodeMarker was renamed to WriteVSCodeMarker and moved to, PesterConfiguration object in Debug section. But it is not implemented and will be removed, I will detect VSCode by env variables

  • Documentation is out of date for all commands

  • Providing parameters to test scripts is not implemented, see parametric scripts

  • JUnit output is missing

  • Noticed more of them? Share please!

Questions?

Use this issue thread, ping me on twitter or #testing

Don't miss a new Pester release

NewReleases is sending notifications on new releases.