Skip to main content
Version: v5

Data driven tests

Migrating from Pester v4? Jump to Migrating from Pester v4.

Pester can generate tests based on data. This can range from providing multiple examples on a single It, to generating whole set of tests based on an external configuration file.

Using -ForEach & -TestCases with hashtable

The most common usage of data driven tests is providing multiple examples to a single It. This way we reuse the body of the test. And decorate the test with examples of its usage. This is superior to copy-pasting tests for each example, because the amount of code to maintain is reduced, the amount of typos is reduced, and it is very easy to add more examples.

To define examples for a test, use -ForEach on an It block. On It block the parameter is also aliased -TestCases for backwards compatibility.

The data are provided as an array of hashtables:

Required code for samples (PowerShell 6 or later)

This module is required for the Emoji and Fruit tests below. Copy and paste this in your console before running the tests.

Get-Module PesterDemoFunctions | Remove-Module
New-Module PesterDemoFunctions -ScriptBlock {
$emojis = @(
@{ Name = 'apple'; Symbol = '🍎'; Kind = 'Fruit' }
@{ Name = 'beaming face with smiling eyes'; Symbol = '😁'; Kind = 'Face' }
@{ Name = 'cactus'; Symbol = '🌵'; Kind = 'Plant' }
@{ Name = 'giraffe'; Symbol = '🦒'; Kind = 'Animal' }
@{ Name = 'pencil'; Symbol = '✏️'; Kind = 'Item' }
@{ Name = 'penguin'; Symbol = '🐧'; Kind = 'Animal' }
@{ Name = 'pensive'; Symbol = '😔'; Kind = 'Face' }
@{ Name = 'slightly smiling face'; Symbol = '🙂'; Kind = 'Face' }
@{ Name = 'smiling face with smiling eyes'; Symbol = '😊'; Kind = 'Face' }
) | ForEach-Object { [PSCustomObject]$_ }

function Get-Emoji ([string]$Name = '*') {
$script:emojis | Where-Object Name -like $Name | ForEach-Object Symbol
}

function Get-EmojiKind {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]$Emoji
)
process {
$script:emojis | Where-Object Symbol -eq $Emoji | Foreach-Object Kind
}
}

$fruitBasket = [System.Collections.ArrayList]@('🍎','🍌','🥝','🥑','🍇','🍐')

function Get-FruitBasket {
$script:fruitBasket
}

function Remove-FruitBasket {
param(
[Parameter(Mandatory = $true)]
[string]$Item
)
$script:fruitBasket.Remove($Item)
}

function Reset-FruitBasket {
$script:fruitBasket = [System.Collections.ArrayList]@('🍎','🍌','🥝','🥑','🍇','🍐')
}
} | Import-Module
Describe "Get-Emoji" {
It "Returns <expected> (<name>)" -ForEach @(
@{ Name = "cactus"; Expected = '🌵'}
@{ Name = "giraffe"; Expected = '🦒'}
) {
Get-Emoji -Name $name | Should -Be $expected
}
}

@() is an array. One test will be generated for each item in the array.

Inside of the @() array, a hashtable is provided @{}. This hashtable can have multiple keys, here it is Name and Expected, and each one of those keys will be defined as a variable in the test. These variables can be used in the test, and also in the <> templates, described below.

The code above will generate tests that are equivalent to this:

Describe "Get-Emoji" {
It "Returns 🌵 (cactus)" {
Get-Emoji -Name cactus | Should -Be '🌵'
}

It "Returns 🦒 (giraffe)" {
Get-Emoji -Name giraffe | Should -Be '🦒'
}
}

Using -ForEach (or -TestCases) we can now very easily provide more examples just by adding more data:

Describe "Get-Emoji" {
It "Returns <expected> (<name>)" -ForEach @(
@{ Name = "cactus"; Expected = '🌵'}
@{ Name = "giraffe"; Expected = '🦒'}
@{ Name = "apple"; Expected = '🍎'}
@{ Name = "pencil"; Expected = '✏️'}
@{ Name = "penguin"; Expected = '🐧'}
@{ Name = "smiling face with smiling eyes"; Expected = '😊'}
) {
Get-Emoji -Name $name | Should -Be $expected
}
}

-ForEach on Describe and Context

The same -ForEach syntax is used on Describe (or Context) to generate those blocks from the provided data:

Describe "Get-Emoji <name>" -ForEach @(
@{ Name = "cactus"; Symbol = '🌵'; Kind = 'Plant' }
@{ Name = "giraffe"; Symbol = '🦒'; Kind = 'Animal' }
) {
It "Returns <symbol>" {
Get-Emoji -Name $name | Should -Be $symbol
}

It "Has kind <kind>" {
Get-Emoji -Name $name | Get-EmojiKind | Should -Be $kind
}
}

Which is equivalent to writing:

Describe "Get-Emoji cactus" {
It "Returns 🌵" {
Get-Emoji -Name cactus | Should -Be '🌵'
}

It "Has kind Plant" {
Get-Emoji -Name cactus | Get-EmojiKind | Should -Be 'Plant'
}
}

Describe "Get-Emoji giraffe" {
It "Returns 🦒" {
Get-Emoji -Name giraffe | Should -Be '🦒'
}

It "Has kind Animal" {
Get-Emoji -Name giraffe | Get-EmojiKind | Should -Be 'Animal'
}
}

Choosing a subset of data

The provided data will be available in both Discovery and Run, which allows us to generate tests based on the current data. Here for example we generate tests in a Context, based on the data provided to the Describe:

Describe "Get-Emoji <name>" -ForEach @(
@{
Name = "cactus";
Symbol = '🌵';
Kind = 'Plant'
Runes = @(
@{ Index = 0; Rune = 127797 }
)
}
@{
Name = "pencil"
Symbol = '✏️'
Kind = 'Item'
Runes = @(
@{ Index = 0; Rune = 9999 }
@{ Index = 1; Rune = 65039 }
)
}
) {
It "Returns <symbol>" {
Get-Emoji -Name $name | Should -Be $symbol
}

It "Has kind <kind>" {
Get-Emoji -Name $name | Get-EmojiKind | Should -Be $kind
}

Context "Runes (each character in multibyte emoji)" -ForEach $runes {
It "Has rune <rune> on index <index>" {
$actual = @((Get-Emoji -Name $name).EnumerateRunes())
$actual[$index].Value | Should -Be $rune
}
}
}

Using -ForEach & -TestCases with an array

-ForEach is most powerful when used with an array of hashtables, but it can take any array. In that case it will define $_ variable, representing the current item. This approach can be used on Describe, Context and It:

Describe "Get-FruitBasket" {
It "Contains <_>" -ForEach '🍎','🍌','🥝','🥑','🍇','🍐' {
Get-FruitBasket | Should -Contain $_
}
}

Describe "Get-FruitBasket" {
Context "Fruit <_>" -ForEach '🍎','🍌','🥝','🥑','🍇','🍐' {
It "Contains <_> by default" {
Get-FruitBasket | Should -Contain $_
}

It "Can remove <_> from the basket" {
Remove-FruitBasket -Item $_
Get-FruitBasket | Should -Not -Contain $_
}
}
AfterAll {
Reset-FruitBasket
}
}
tip

-ForEach defines the $_ variable even when used with an array of hashtables. In that case $_ will be set to the current hashtable.

Using <> templates

Test and block names can contain values surrounded by <> in their name. Those will be considered a template, and will be expanded. Any variable available in scope can be expanded using this syntax, not just the data from -ForEach:

BeforeAll {
$apple = '🍎'
}

Describe "<apple>" {
It "<apple> <animal>" -ForEach @(
@{ Animal = "🐛" }
@{ Animal = "🐶" }
) {
# ...
}
}
Describing 🍎
[+] 🍎 🐛 5ms (3ms|2ms)
[+] 🍎 🐶 2ms (1ms|1ms)

Additionally, the expansion of the string is postponed after the setup for the current block or test. This enables you to expand variables defined in BeforeAll, and BeforeEach of the current block, even though the code is visually below the name of the block:

Describe "<banana>" {
BeforeAll {
$banana = '🍌'
}

BeforeEach {
$giraffe = '🦒'
}

It "<banana> <giraffe> <animal>" {
# ...
}
}
Describing 🍌
[+] 🍌 🦒 7ms (5ms|3ms)

<_> in template

The current item in the provided array is represented by $_ and can be expanded by using <_> template.

Describe "Animals " {
It "<_>" -ForEach @("🐛", "🐶") {
# ...
}
}
Describing Animals
[+] 🐛 7ms (2ms|5ms)
[+] 🐶 2ms (1ms|1ms)

Using dot-navigation in <> template

The <> templates allow to navigate through the provided object by using dot-notation. This is useful when the data are not just a flat structure:

Describe "Animals" {
It "A <animal.emoji> (<name>) goes <animal.sound>" -ForEach @(
@{
Name = "cow"
Animal = @{
Sound = "Mooo"
Emoji = "🐄"
}
}

@{
Name = "fox"
Animal = @{
Sound = "Ring-ding-ding-ding-dingeringeding!"
Emoji = "🦊"
}
}
) {
# ...
}
}
Describing Animals
[+] A 🐄 (cow) goes Mooo 12ms (1ms|11ms)
[+] A 🦊 (fox) goes Ring-ding-ding-ding-dingeringeding! 2ms (1ms|1ms)

Escaping

Before the template string is expanded every $ and ` in the text is escaped. This allows you to put literals, such as $null in your test names, and still use the template to expand the value:

Describe "Fruit" -ForEach "🍎", "🍐" {
# with single quoted string
It 'Getting <_> returns $null' {
# ...
}

# with double quoted string
It "Getting <_> returns `$null" {
# ...
}
}
Describing Fruit
[+] Getting 🍎 returns $null 3ms (1ms|2ms)
[+] Getting 🍎 returns $null 2ms (1ms|1ms)

Describing Fruit
[+] Getting 🍐 returns $null 3ms (1ms|3ms)
[+] Getting 🍐 returns $null 1ms (1ms|1ms)

To avoid a < from being considered a template use `< to escape it. This allows you to put greater than and less than in your test names. Or to wrap values in <>:

# with single quoted string
Describe 'When x `< 4, x: <_>' -ForEach @(1..4) {
It 'x: `<<_>`>' {
# ...
}
}

# with double quoted string
Describe "When x ``< 4, x: <_>" -ForEach @(1..4) {
It "x: ``<<_>``>" {
# ...
}
}
Describing When x < 4, x: 1
[+] x: <1> 3ms (1ms|3ms)

Describing When x < 4, x: 2
[+] x: <2> 6ms (0ms|6ms)

Describing When x < 4, x: 3
[+] x: <3> 5ms (1ms|5ms)

Describing When x < 4, x: 4
[+] x: <4> 4ms (0ms|4ms)

Internals

The value in the string is escaped by adding ` before every $ and `. Every template is prefixed by $, and then the string is expanded:

"<x> should not be `$null"

# because this is double quoted string it will be
# expanded by powershell to

<x> should not be $null

# then escaped by us to
<x> should not be `$null

# template is replaced
$x should not be `$null

# and then it is evaluated in the appropriate script scope
$x = 1
& { "$x should not be `$null" }

# giving the final text
1 should not be $null

Providing external data to tests

.Tests.ps1 script is a PowerShell script as any other and you can use the param block in it. Script parameters are available in both Discovery and Run. This is very useful when reusing a single test file with multiple data sets. For example when writing a generic test suite that ensures that correct style is used in a given script file:

CodingStyle.Tests.ps1
param (
[Parameter(Mandatory)]
[string] $File
)

BeforeAll {
$content = Get-Content $File
}

Describe "File - <file>" {
Context "Whitespace" {
It "There is no extra whitespace following a line" {
# ...
}

It "File ends with an empty line" {
# ...
}
}
}

The external data are then provided to the test by -Data parameter on New-PesterContainer, in the same manner -ForEach would be used on Describe or It:

$container = New-PesterContainer -Path 'CodingStyle.Tests.ps1' -Data @{ File = "Get-Emoji.ps1" }
Invoke-Pester -Container $container -Output Detailed
Running tests from 'C:\p\style\CodingStyle.Tests.ps1'
Describing File - Get-Emoji.ps1
Context Whitespace
[+] There is no extra whitespace following a line 30ms (1ms|30ms)
[+] File ends with an empty line 2ms (1ms|1ms)
Tests completed in 445ms
Tests Passed: 2, Failed: 0, Skipped: 0 NotRun: 0
note

$PSBoundParameters is not available in Pester tests. Reference parameters by their variables like $File in the example above.

Re-using the same test file

In the scenario above we tested just a single file, but chances are we want to run those style tests for every file in the repository. This can be achieved by generating one data item for each file:

$files = 'Get-Emoji.ps1', 'Get-Planet.ps1'
$containers = $files | ForEach-Object {
New-PesterContainer -Path 'CodingStyle.Tests.ps1' -Data @{ File = $_ }
}

Invoke-Pester -Container $containers -Output Detailed

This will run the test file twice, once for each file. You could even grab all .ps1 files in the current Path and test them, just by using $files = Get-ChildItem -Filter '*.ps1' -Recurse.

The -Data parameter takes an array of hashtables. As an alternative to the code above, you also attach all the test data to a single container:

$files = 'Get-Emoji.ps1', 'Get-Planet.ps1'
$data = $files | ForEach-Object {
@{ File = $_ }
}

$container = New-PesterContainer -Path 'CodingStyle.Tests.ps1' -Data $data
Invoke-Pester -Container $container -Output Detailed

There is no advantage in using one over the other, choose the one that suits your needs better.

Re-using the same data set

In the example above we had a single test file, and multiple different data sets. We can also have the opposite. A single data set used by multiple test files:

$data =  @{ File = "Get-Emoji.ps1" }
$containers = @(
(New-PesterContainer -Path 'CodingStyle.Tests.ps1' -Data $data)
(New-PesterContainer -Path 'BadWords.Tests.ps1' -Data $data)
)
Invoke-Pester -Container $containers -Output Detailed

# or simply

$data = @{ File = "Get-Emoji.ps1" }
$container = New-PesterContainer -Path 'CodingStyle.Tests.ps1', 'BadWords.Tests.ps1' -Data $data
Invoke-Pester -Container $container -Output Detailed

Wildcards

New-PesterContainer resolves the -Path parameter the same way Invoke-Pester does. You can use it to provide a wildcard path to run all files matching the pattern. For example to run all *Style tests against all files in the current directory:

$files = Get-ChildItem -Filter '*.ps1' -Recurse
$data = $files | ForEach-Object {
@{ File = $_.FullName }
}


$containers = New-PesterContainer -Path '*Style.Tests.ps1' -Data $data
Invoke-Pester -Container $containers -Output Detailed

BeforeDiscovery

In Pester v4 it was normal to place code directly on top of the script file, or directly into the body of Describe and Context. Pester v5 discourages this, because all code directly in script or in Describe / Context body will run during Discovery. This leads to unexpected results.

BUT in some cases, especially when generating tests, putting code directly in script body is okay. BeforeDiscovery was introduced to show that the code is like this intentionally, and not by accident. Wrapping code into BeforeDiscovery has no impact on its execution. It should be used everywhere where code is intentionally placed outside of a Pester controlled leaf-block (see Discovery and Run). That is on top of files, or directly in body of Describe / Context.

Here for example I generate one Describe for each script file, instead of passing the list of files externally. I need this code to run during Discovery to generate those blocks. BeforeDiscovery is used there to show that this code placement is intentional:

BeforeDiscovery {
$files = Get-ChildItem -Path $PSScriptRoot -Filter '*.ps1' -Recurse
}

Describe "File - <_>" -ForEach $files {
Context "Whitespace" {
It "There is no extra whitespace following a line" {
# ...
}

It "File ends with an empty line" {
# ...
}
}
}

Migrating from Pester v4

There are two main things to take into account when Data driven tests in Pester v5: The execution is not top down like in V4 but done in two phases Discovery and Run. Variables from Discovery are not available in Run, unless you explicitly attach them to a Describe, Context or It using Foreach or TestCases

This has big impact the classic pattern of using foreach to generate tests.

Execution is not top down

Pester v5 introduces a new two phase execution. In the first phase called Discovery, it will run your whole test script from top to bottom. It will also run all ScriptBlocks that you provided to any Describe, Context and BeforeDiscovery. It will collect the ScriptBlocks you provided to It, BeforeAll, BeforeEach, AfterEach and AfterAll, but won't run them until later.

PhaseV4V5 DiscoveryV5 Run
BeforeAll✔️✔️
BeforeEach✔️✔️
It✔️✔️
AfterEach✔️✔️
AfterAll✔️✔️
BeforeDiscoveryN/A✔️
Describe✔️✔️
Context✔️✔️
Other✔️✔️

Other, is any script outside of It, BeforeAll, BeforeEach, AfterEach and AfterAll. You see that this code behaves like the new BeforeDiscovery block and thus for clarity on when script will be run its advised to use the BeforeDiscovery.

In simple terms this means that the script below will run until the last line during Discovery. But no test will be executed:

Write-Host "We are starting with discovery."
Describe "d" {
Write-Host "We are finding tests in this describe."
It "i" {
Write-Host "I am running!"
}

Write-Host "We are leaving this describe."
}

Write-Host "We are done with discovery!"
Starting discovery in 1 files.
Discovering in C:\p\g.Tests.ps1.
We are starting with discovery.
We are finding tests in this describe.
We are leaving this describe.
We are done with discovery!
Found 1 tests. 35ms
Discovery finished in 43ms.

Running tests from 'C:\p\g.Tests.ps1'
Describing d
I am running!
[+] i 5ms (2ms|3ms)
Tests completed in 256ms

Notice that "We are done with discovery!" is placed at the end of the script, but is printed before "I am running!". This is because we postpone the execution of It until the Run phase.

Variables defined during Discovery are not available in Run

Defining a variable directly in the body of the script, will make it available during Discovery, but it won't be available during Run. You can see that the test failed because $name is not defined.

$name = "Jakub"

Describe "d" {
It "My name is: $name" {
$name | Should -be "Jakub"
}
}
Starting discovery in 1 files.
Discovering in C:\p\g.Tests.ps1.
Found 1 tests. 21ms
Discovery finished in 65ms.

Running tests from 'C:\p\g.Tests.ps1'
Describing d
[-] My name is: Jakub 106ms (103ms|3ms)
Expected 'Jakub', but got $null.
at $name | Should -be "Jakub", C:\p\g.Tests.ps1:5
at <ScriptBlock>, C:\p\g.Tests.ps1:5
Tests completed in 436ms
Tests Passed: 0, Failed: 1, Skipped: 0 NotRun: 0

This is especially confusing because $name was correctly expanded in the name of the test. But if you think about it, it is logical. The script is running till the end, so the string "My name is: $name" is expanded during Discovery where the $name variable is available.

Moving away from foreach

Those two limitations mean that this pattern, is difficult to use:

$files = Get-ChildItem "../src/*.ps1" -Recurse
foreach ($file in $files) {
Describe "$file" {
It "$file - has help" {
$content = Get-Content $file
# ...
}
}
}

The $file variable won't be defined in the It and the test will fail. You can work around this problem by specifying a single item -ForEach to attach the value to the block. -ForEach (and -TestCases) will inspect the provided value, and if it is a hashtable, it will define a variable for each key in that hashtable.

$files = Get-ChildItem "../src/*.ps1" -Recurse
foreach ($file in $files) {
Describe "$file" -ForEach { File = $file } {
It "$file - has help" {
$content = Get-Content $file
# ...
}
}
}

This will take the value of $file during Discovery and attach it to that Describe block, and then during Run it will define a new variable $file with the same value.

Next step is to move to -ForEach functionality entirely:

BeforeDiscovery {
$files = Get-ChildItem "../src/*.ps1" -Recurse
}

Describe "<file>" -ForEach $files {
BeforeAll {
# Renaming the automatic $_ variable to $file
# to make it easier to work with
$file = $_
}
It "<file> - has help" {
$content = Get-Content $file
# ...
}
}

There are three notable changes:

  • $files are defined inside of BeforeDiscovery block, to show that this code is running in Discovery intentionally, and not by accident

  • $files is provided directly to -ForEach. This will generate one Describe for each item in $file, and will define the current item as $_ which is available in both Discovery and Run.

  • The names of tests are using the <> template to expand the variable $file during Run. Any variable in scope can be used for that expansion, including the variables defined in the BeforeAll that is contained directly inside of the current Describe.

Another variation on foreach

One more common approach is to put common test setup directly in the Describe block to ensure that the file will be loaded only once:

$files = Get-ChildItem "../src/*.ps1" -Recurse
foreach ($file in $files) {
Describe "$file" {
$content = Get-Content $file
It "$file - has help" {
# ...
}
}
}

This approach can still be easily converted to Pester v5:

BeforeDiscovery {
$files = Get-ChildItem "../src/*.ps1" -Recurse
}

Describe "<file>" -ForEach $files {
BeforeAll {
# Renaming the automatic $_ variable to $file
# to make it easier to work with
$file = $_
$content = Get-Content $file
}
It "<file> - has help" {
# ...
}
}