PowerShell

Converting Pester V4 to Pester V5 basics

Now that Pester V5 is out, I decided that I need to make sure that my Pester tests for all my modules keep on running correctly. Some substantial changes in Pester add new features, changing some things, but that also means all the tests that you have defined most likely will need a small push to get it up and running again on Pester V5. Starting this blog post, I wanted to mention that I am by no means an expert on Pester, but I do use it for some time now for most of my projects.  I am using basic functionality, but even that basic functionality stops working once you upgrade from PesterV4 to PesterV5, so I thought I would save you some time and give you a small overview of how you can quickly fix it. It works for me, but you may have to find your way to fix things.

Pester V5 - Invoke-Pester changes

The first thing that changes is the Invoke-Pester function itself. You no longer have EnableExit, you no longer have Script parameter.

$PSVersionTable.PSVersion

$ModuleName = (Get-ChildItem $PSScriptRoot\*.psd1).BaseName
$RequiredModules = @(
    'PSSharedGoods'
    'Pester'
)
foreach ($_ in $RequiredModules) {
    if ($null -eq (Get-Module -ListAvailable $_)) {
        Write-Warning "$ModuleName - Downloading $_ from PSGallery"
        Install-Module -Name $_ -Repository PSGallery -Force -SkipPublisherCheck
    }
    Import-Module $_ -Force
}
Import-Module $PSScriptRoot\Testimo.psd1 -Force

$result = Invoke-Pester -Script $PSScriptRoot\Tests -Verbose -EnableExit

if ($result.FailedCount -gt 0) {
    throw "$($result.FailedCount) tests failed."
}

If you try to run it without any changes you will get an error:

Invoke-Pester : A parameter cannot be found that matches parameter name ‘Script'.

That means we need to change it to supported parameters

Invoke-Pester [[-Path] <string[]>] [-ExcludePath <string[]>] [-TagFilter <string[]>] [-ExcludeTagFilter <string[]>] [-FullNameFilter <string[]>] [-CI] [-Output {Diagnostic | Detailed | Normal | Minimal | None}] [-PassThru]  [<CommonParameters>]

Or

Invoke-Pester [-Configuration <PesterConfiguration>]  [<CommonParameters>]

Knowing what options we have, I can now simply change Path, and the command should mostly behave the same. However, what also changed is now Invoke-Pester runs with minimal output only – meaning it will only show errors if not specified otherwise. And it makes sense – we're mostly interested in errors, right? We don't need 500 tests that passed, but only 42 that didn't. If you still need to see all of it, you need to additionally specify the Output parameter and decide how much information do you need.

$PSVersionTable.PSVersion

$ModuleName = (Get-ChildItem $PSScriptRoot\*.psd1).BaseName
$RequiredModules = @(
    'PSSharedGoods'
    'Pester'
)
foreach ($_ in $RequiredModules) {
    if ($null -eq (Get-Module -ListAvailable $_)) {
        Write-Warning "$ModuleName - Downloading $_ from PSGallery"
        Install-Module -Name $_ -Repository PSGallery -Force -SkipPublisherCheck
    }
    Import-Module $_ -Force
}
Import-Module $PSScriptRoot\Testimo.psd1 -Force

$result = Invoke-Pester -Path $PSScriptRoot\Tests #-Output Detailed

if ($result.FailedCount -gt 0) {
    throw "$($result.FailedCount) tests failed."
}

A simple change and we can see that our command worked, and we saw 420 silently passed and 42 that failed.

Pester V5 - No code in Describe block

The second problem I had with the conversion from PesterV4 to PesterV5 was the information in the documentation that you shouldn't use any code in Describe blocks. That's going to be a problem because I see it as the only way to build tests dynamically. I've reached out to @nohwnd, and he mentioned that it's just good practice, but if I know what I'm doing, it's still allowed. As far as I understand it, the drawback will be – the code in Describe block will run twice. Once during the discovery phase, the second time during real runtime. So far, so good.

As you can see below, I have code defined on lines 2-3 that dynamically test whole Hash for me. But since @nohwnd said it's OK to run code in Describe block when I know what I am doing (and of course I know what I am doing, right?), I run with it.

Describe 'Testimo Sources' {
    $TestimoSources = Get-TestimoSources -Advanced
    foreach ($Source in $TestimoSources) {
        It "Source $($Source.Source) should contain Enable key and be enabled or disabled" {
            $Source.Advanced.Enable | Should -BeIn @($True, $False)
        }
        It "Source $($Source.Source) should contain Source Key" {
            $Source.Advanced.Keys | Should -Contain 'Source'
        }
        It "Source $($Source.Source) should contain 6 in Source Details" {
            $Keys = @(
                'Area'
                'Category'
                'Severity'
                'RiskLevel'
                'Description'
                'Resolution'
                'Resources'
            )
            $Source.Advanced.Source.Details.Keys | Sort-Object | Should -Be ($Keys | Sort-Object)
        }
    }
}

The only problem is, I have all my tests failing – 231 tests. On PesterV4, only some of them failed.

Again a small change was done to how PesterV5 handles things. If you need to pass variables into It block, you need to use a parameter called TestCases. If you don't do that, the variable you have defined outside of It block will not be available in it, and since your tests require it – they will fail.

Describe 'Testimo Sources' {
    $TestimoSources = Get-TestimoSources -Advanced
    foreach ($Source in $TestimoSources) {
        It "Source $($Source.Source) should contain Enable key and be enabled or disabled" -TestCases @{ Source = $Source } {
            $Source.Advanced.Enable | Should -BeIn @($True, $False)
        }
        It "Source $($Source.Source) should contain Source Key" -TestCases @{ Source = $Source } {
            $Source.Advanced.Keys | Should -Contain 'Source'
        }
        It "Source $($Source.Source) should contain 6 in Source Details" -TestCases @{ Source = $Source } {
            $Keys = @(
                'Area'
                'Category'
                'Severity'
                'RiskLevel'
                'Description'
                'Resolution'
                'Resources'
            )
            $Source.Advanced.Source.Details.Keys | Sort-Object | Should -Be ($Keys | Sort-Object)
        }
    }
}

Do you see how each It block now has additional TestCases parameter that's defined as hashtable with mapping outside variable to inside variables? That's how you're supposed to pass data to It blocks now. Now we just need to follow this approach and change all my tests to this new approach. Except, the first test was a small one. I've got plenty of other tests that repeat the same pattern over and over. Here's another one that would require me to change It blocks nine times, and every time I have to do that for a new test I write, I will die a little bit inside.

Describe 'Testimo Configuration for Forest' {
    # Preparations
    $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru
    $TestimoConfiguration = & $ImportedModule {
        $Script:TestimoConfiguration
    }

    foreach ($Key in $TestimoConfiguration['Forest'].Keys) {
        It -Name "Test Source $Key should not be NULL" {
            $TestimoConfiguration['Forest'].$Key | Should -Not -Be $Null
        }
        It -Name "Test Source $Key should contain Enable" {
            $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Enable'
        }
        It -Name "Test Source $Key should contain Source" {
            $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Source'
        }
    }
}

Describe 'Testimo Configuration for Domains' {
    # Preparations
    $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru
    $TestimoConfiguration = & $ImportedModule {
        $Script:TestimoConfiguration
    }

    foreach ($Key in $TestimoConfiguration['Domain'].Keys) {
        It -Name "Test Source $Key should not be NULL" {
            $TestimoConfiguration['Domain'].$Key | Should -Not -Be $Null
        }
        It -Name "Test Source $Key should contain Enable" {
            $TestimoConfiguration['Domain'].$Key.Keys | Should -Contain 'Enable'
        }
        It -Name "Test Source $Key should contain Source" {
            $TestimoConfiguration['Domain'].$Key.Keys | Should -Contain 'Source'
        }
    }
}

Describe 'Testimo Configuration for DomainControllers' {
    # Preparations
    $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru
    $TestimoConfiguration = & $ImportedModule {
        $Script:TestimoConfiguration
    }

    foreach ($Key in $TestimoConfiguration['DomainControllers'].Keys) {
        It -Name "Test Source $Key should not be NULL" {
            $TestimoConfiguration['DomainControllers'].$Key | Should -Not -Be $Null
        }
        It -Name "Test Source $Key should contain Enable" {
            $TestimoConfiguration['DomainControllers'].$Key.Keys | Should -Contain 'Enable'
        }
        It -Name "Test Source $Key should contain Source" {
            $TestimoConfiguration['DomainControllers'].$Key.Keys | Should -Contain 'Source'
        }
    }
}

But fortunately, we can use, not very known, but very useful feature of PowerShell called $PSDefaultParameterValues. It allows you to define default parameters that will be used if no parameter is specified.

$PSDefaultParameterValues = @{
     "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration }
}

Do you see how I tell it that from now on It function parameter called TestCases needs always to be assigned Hash with Keys called Key and TestimoConfiguration and their respective values? You just need to define it once per Describe block (where it makes sense) and voila. Every It block will execute those parameters assigned to it.

Describe 'Testimo Configuration for Forest' {
    # Preparations
    $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru
    $TestimoConfiguration = & $ImportedModule {
        $Script:TestimoConfiguration
    }

    foreach ($Key in $TestimoConfiguration['Forest'].Keys) {
        $PSDefaultParameterValues = @{
            "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration }
        }
        It -Name "Test Source $Key should not be NULL" {
            $TestimoConfiguration['Forest'].$Key | Should -Not -Be $Null
        }
        It -Name "Test Source $Key should contain Enable" {
            $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Enable'
        }
        It -Name "Test Source $Key should contain Source" {
            $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Source'
        }
    }
}

Describe 'Testimo Configuration for Domains' {
    # Preparations
    $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru
    $TestimoConfiguration = & $ImportedModule {
        $Script:TestimoConfiguration
    }

    foreach ($Key in $TestimoConfiguration['Domain'].Keys) {
        $PSDefaultParameterValues = @{
            "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration }
        }
        It -Name "Test Source $Key should not be NULL" {
            $TestimoConfiguration['Domain'].$Key | Should -Not -Be $Null
        }
        It -Name "Test Source $Key should contain Enable" {
            $TestimoConfiguration['Domain'].$Key.Keys | Should -Contain 'Enable'
        }
        It -Name "Test Source $Key should contain Source" {
            $TestimoConfiguration['Domain'].$Key.Keys | Should -Contain 'Source'
        }
    }
}

Describe 'Testimo Configuration for DomainControllers' {
    # Preparations
    $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru
    $TestimoConfiguration = & $ImportedModule {
        $Script:TestimoConfiguration
    }

    foreach ($Key in $TestimoConfiguration['DomainControllers'].Keys) {
        $PSDefaultParameterValues = @{
            "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration }
        }
        It -Name "Test Source $Key should not be NULL" {
            $TestimoConfiguration['DomainControllers'].$Key | Should -Not -Be $Null
        }
        It -Name "Test Source $Key should contain Enable" {
            $TestimoConfiguration['DomainControllers'].$Key.Keys | Should -Contain 'Enable'
        }
        It -Name "Test Source $Key should contain Source" {
            $TestimoConfiguration['DomainControllers'].$Key.Keys | Should -Contain 'Source'
        }
    }
}

And that's it. Now when I rerun my Pester tests, it's beautiful and green, or not so green really, but giving me enough to continue using pester with two small changes.

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

1 miesiąc ago

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, "Why do I still have…

2 miesiące ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

7 miesięcy ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

11 miesięcy ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 rok ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 rok ago