Skip to main content
Version: v5

Discovery and Run

Pester runs your test files 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 Pester 5 and enables many others to be implemented in the future.

To reap the benefits, there are two 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.

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

Execution order

In Pester v4 the execution of your test code was mostly top-down, with the exception of all the Before* and After* blocks. Those blocks were picked up by Pester using AST and "moved" to the place where they needed to run.

For example placing an AfterAll at the top of a Describe would not run it at the top, but instead it would correctly run it as the last thing in that Describe wrapped in a try finally.

Pester v5 takes this to the next level and manipulates the execution of your test file in a more complex way. This has good reason, but makes understanding how your code executes quite complex, especially when you don't follow the two rules listed above.

Discovery

Take for example this test file. When executed it will discover two tests inside of this file, and run them:

BeforeAll {
. $PSCommandPath.Replace('.Tests.ps1','.ps1')
}

Describe "Get-Emoji" {
It "Returns <expected> (<name>)" -TestCases @(
@{ Name = "cactus"; Expected = '🌵'}
@{ Name = "giraffe"; Expected = '🦒'}
) {
Get-Emoji -Name $name | Should -Be $expected
}
}

Write-Host "Discovery done."

This is what happens step by step when you run Get-Emoji.Tests.ps1:

  • The script file Get-Emoji.Tests.ps1 is invoked
  • BeforeAll function runs. It saves the ScriptBlock provided to it, but does not execute it yet.
  • Describe function runs. It invokes the ScriptBlock provided to it, to be able to collect information about the tests contained inside of that scriptblock. Describe and Context scriptblocks are the only scriptblocks that are invoked during Discovery.
  • All parameters provided to It are evaluated by PowerShell:
    • -Name is evaluated:
      • if double quoted string is used ("") all variables and sub-expressions in the string are expanded
      • if single quotes string is used ('') it is taken as it is
    • -TestCases are evaluated:
      • all "" strings are expanded
      • all commands are executed, if any are used
  • It function runs. The ScriptBlock provided (the body of the test) is saved but not executed yet. Two tests are generated. One for each item in -TestCases array.
  • Write-Host runs and prints "Discovery done."

At this point we are done with Discovery. As you can see discovery is just a "trick". We run the whole script to the end, but don't invoke any ScriptBlock, except for the one associated with Describe.

At the end of Discovery we have collected all the tests and setups that are contained in the file, and stored them in the internal data of Pester module. The internal representation looks something like this:

Container: Get-Emoji.Tests.ps1
BeforeAll: `{ . $PSCommandPath.Replace('.Tests.ps1','.ps1') }
AfterAll: <none>
Blocks:
Block: Get-Emoji
BeforeAll: <none>
BeforeEach: <none>
AfterEach: <none>
AfterAll: <none>
Tests:
Test: Returns <expected> (<name>)
Data: @{ Name = "cactus"; Expected = '🌵'}
ScriptBlock: { Get-Emoji -Name $name | Should -Be $expected }

Test: Returns <expected> (<name>)
Data: @{ Name = "giraffe"; Expected = '🦒'}
ScriptBlock: { Get-Emoji -Name $name | Should -Be $expected }

Now we would go through all the objects, check filters and propagate them down and back up. This gives us the final overview of what needs to run. In this case there are no filters, so nothing changes and all tests will be invoked.

At this point we are done with the Discovery phase. We invoked the script to the end, and evaluated all the -TestCases and -Name strings if they have variables in them and use double quotes.

Run

In the run phase we take the internal tree of containers, blocks and tests and run them. We need to do quite a lot of work to reconstruct the correct scoping and to ensure that everything runs safely. At a high level this is what happens:

  • Run the BeforeAll ScriptBlock, this imports the tested function
  • Create new scope to isolate the test from other tests
    • Invoke the It ScriptBlock, this runs the first test
    • Return to the previous scope
  • Create new scope to isolate the test from other tests
    • Invoke the It ScriptBlock, this runs the second test
    • Return to the previous scope
  • Return to the previous scope

We are done. All tests in this container are executed.

Common gotchas

BeforeAll and -TestCases

-TestCases are evaluated during Discovery, but BeforeAll won't run until the Run phase. Using variables set in BeforeAll in -TestCases (or -ForEach) won't work. The variable from BeforeAll simply won't be defined until much after -TestCases and -ForEach are evaluated.

Generating test and blocks via foreach keyword

A common pattern in Pester v4 is to use foreach to generated tests and blocks based on external data, for example like this:

BeforeAll {
# check all script files
$files = Get-ChildItem *.ps1
}

foreach ($file in $files) {
Describe "$file is correct" {
It "has empty line at end" {
# ...
}

It "has UTF-8 encoding" {
# ...
}
}
}

In this case the foreach is evaluated during Discovery because we simply run the whole script file. But BeforeAll won't be invoked until Run, so the $files variable is not defined during Discovery and the tests are not generated. This can be solved by defining the code that is used to generate tests into BeforeDiscovery block. See Data driven tests.

Variables are not available in test

The example above has one more problem, if we move the setup to BeforeDiscovery, we will generate the tests correctly, and the foreach will define $file variable. But we won't be able to use the $file variable in the It. We need to attach it to the It using -TestCases with a single test case:

BeforeDiscovery { # <- this will run during Discovery
# check all script files
$files = Get-ChildItem *.ps1
}

foreach ($file in $files) {
Describe "$file is correct" {
It "has empty line at end" -TestCases @{ File = $file } { # <- we pass $file data to the test
# ...
}

It "has UTF-8 encoding" {
# ...
}
}
}

Both of the problems above are avoidable by using -ForEach, see Data driven tests.