Tuesday, February 11, 2025
HomePowershellConstruct a Scalable PowerShell Pester Testing Framework

Construct a Scalable PowerShell Pester Testing Framework


If you end up spending extra time sustaining your Pester exams than really creating new ones, this put up is for you. On this put up, I’ll share a venture I’ve been engaged on with Devolutions.

Backstory

We had an issue at Devolutions. The flagship PowerShell module Devolutions.PowerShell lacked Pester exams. I do know, I do know. They did have C# exams however that’s one other story.

As a advisor for Devolutions targeted on PowerShell, I used to be requested to create a set of Pester exams for use within the CI/CD pipeline and run earlier than manufacturing deployment. No downside, I assumed. I’ve used the module a couple of instances, and it couldn’t be that tough. I used to be unsuitable.

2025-01-29_09-27-59.png 2025-01-29_09-27-59.png

With none particular request, simply “construct some exams,” I got down to construct exams for the entire module, solely to seek out that it had almost 500 instructions! This was going to take some time.

Since I at all times wished to construct options not only for right now however for the long run, I didn’t wish to throw a bunch of exams right into a single script and name it a day. That script can be large!

As an alternative, earlier than writing a single take a look at, I wished to design a framework I may use that might:

  1. Be scalable – Myself or others may simply add exams to the framework with out a lot thought.
  2. To stop code duplication, this venture must be DRY (don’t repeat your self). I didn’t wish to duplicate any code, so I as a substitute used shared scripts, features, and so forth.
  3. To make use of data-driven exams: Knowledge-driven exams promote code reusability by defining exams in a knowledge construction after which having exams reference that construction as a substitute of duplicating testing code for each take a look at.
  4. Be modular – All exams should be break up up into some construction that might stop having to scroll by means of pointless code to seek out what you’re in search of.
  5. To help surroundings setup and teardown situations – These exams are end-to-end (E2E)/integration/acceptance exams, no matter you need. They’re testing the end-user utilization; there are not any unit exams, so that they will need to have numerous issues in place within the surroundings to run.
  6. To help operating teams of exams with out operating them unexpectedly.

These necessities result in the Pester testing framework for Devolutions.

Stipulations

When you’d like to make use of this framework, I can’t assure it’ll work 100% except you’re working in the identical surroundings I’m. To make use of this framework, I used:

  • PowerShell v7.4.5 on Home windows
  • Pester 5.6.1

This can nonetheless work on Home windows PowerShell and earlier variations of Pester v5, however there are not any ensures!

Framework Overview

This framework consists of 4 parts: a caller script, numerous take a look at definition scripts, an non-obligatory script to retailer helper features, and non-obligatory earlier than all / earlier than every scripts. Every of those parts is structured like this within the file system:

📁 root/
   📄 caller.exams.ps1
   📄 helper_functions.ps1
   📁 test_definitions/
      📁 group1/
         📄 beforeall.exams.ps1
         📄 beforeeach.exams.ps1
         📄 core.exams.ps1
         📄 subgroup.exams.ps1

Whenever you invoke the caller script:

Invoke-Pester -Path root/caller.exams.ps1

The caller script:

  1. Finds the entire take a look at definition scripts in every group.
  2. Finds all beforeeach and beforeall scripts for every group.
  3. Creates Pester contexts for every group and subgroup.
    context 'group' {
        context 'subgroup' {
    
        }
    }
  4. Invokes the beforeall script as soon as earlier than any take a look at runs in a gaggle.
  5. Invokes the beforeeach script earlier than each take a look at within the group.
  6. Lastly, it invokes the take a look at assertions outlined in every take a look at definition.

The Caller Check Script

The caller script is Pester’s invocation level. It’s the script that Invoke-Pester calls when you could invoke any exams throughout the framework.

The caller script is damaged out into X areas:

The BeforeDiscovery Block

One activity of the caller script is to seek out all of the take a look at definitions and what the Pester’s discovery section is for. Contained in the script’s BeforeDiscovery block, take a look at definitions are gathered, in addition to any earlier than every or earlier than all scripts.

BeforeDiscovery {

    # Initialize hashtable to retailer all take a look at definitions
    $exams = @{}

    # Iterate by means of take a look at group directories (e.g. datasources, entries)
    Get-ChildItem -Path "$PSScriptRoottest_definitions" -Listing -PipelineVariable testGroupDir | ForEach-Object {
        $testGroup = $testGroupDir.BaseName
        $exams[$testGroup] = @{}

        # Load beforeall script for take a look at group if it exists
        # This script runs as soon as earlier than all exams within the group
        $testGroupBeforeAllScriptPath = Be part of-Path -Path $testGroupDir.FullName -ChildPath 'beforeall.ps1'
        if (Check-Path -Path $testGroupBeforeAllScriptPath) {
            $exams[$testGroup]['beforeall'] += . $testGroupBeforeAllScriptPath
        }

        # Load beforeeach script for take a look at group if it exists
        # This script runs earlier than every particular person take a look at within the group
        $testGroupBeforeEachScriptPath = Be part of-Path -Path $testGroupDir.FullName -ChildPath 'beforeeach.ps1'
        if (Check-Path -Path $testGroupBeforeEachScriptPath) {
            $exams[$testGroup]['beforeeach'] += . $testGroupBeforeEachScriptPath
        }

        # Load all take a look at definition information within the group listing
        Get-ChildItem -Path $testGroupDir.FullName -Filter '*.ps1' -PipelineVariable testDefinitionFile | ForEach-Object {
            $exams[$testGroup][$testDefinitionFile.BaseName] = @()
            $exams[$testGroup][$testDefinitionFile.BaseName] += . $testDefinitionFile.FullName
        }
    }
}

The BeforeAll Block

The BeforeAll block runs to make any helper features obtainable to the caller script or take a look at definitions. In Pester v5, this activity can’t be in BeforeDiscovery; in any other case, it wouldn’t be obtainable to the exams.

# Load helper features used throughout all exams
BeforeAll {
    . (Be part of-Path $PSScriptRoot -ChildPath "_helper_functions.ps1")
}

The Describe Block and Contexts

Every surroundings configuration you could run exams towards is break up into contexts with take a look at teams beneath every of these.

For the Devolutions PowerShell module, since we have to take a look at cmdlets towards many various kinds of knowledge sources, I created contexts by knowledge supply, however you should utilize something right here. Then, utilizing Pester’s ForEach parameter, every take a look at definition folder is a context, as is every subgroup. Then, every take a look at is outlined beneath as it blocks.


Discover the the place() technique on the it block. The place({ $_.environments -contains 'xxx' -or $_.environments.depend -eq 0}). That is the place the construction of every definition comes into play. This half is the place we designate which exams run by which surroundings.


# Primary take a look at container for all exams
Describe 'RDM' {

    # Assessments that run towards an surroundings
    Context 'Environment1' -Tag 'Environment1' {

        BeforeEach {
            ## Do stuff to execute earlier than every take a look at on this content material. For instance, that is used to arrange a particular knowledge supply for Devolutions Distant Desktop Supervisor
        }

        # Clear up
        AfterAll {

        }

        # Run every take a look at group towards the surroundings
        Context '<_>' -ForEach $exams.Keys -Tag ($_) {
            $testGroup = $_

            # Run every take a look at subgroup (core, properties, and so forth)
            Context '<_>' -ForEach $exams[$testGroup].Keys -Tag ($_) {
                $testSubGroup = $_

                # Run exams marked for this surroundings or all
                It '<title>' -ForEach ($exams[$testGroup][$testSubGroup]).The place({ $_.environments -contains 'xxx' -or $_.environments.depend -eq 0}) {
                    & $_.assertion
                }
            }
        }
    }

    ## Different environments. You probably have particular configurations some exams will need to have, you possibly can outline them right here by creating extra context blocks.
}

Check Definitions

Subsequent, you’ve essentially the most important half: the exams! The exams are created in subgroups (subgroup.exams.ps1) inside of every group folder and should be created as an array of hashtables with the next construction:

@(
    @{
        'title' = 'creates a factor'
        'environments' = @() ## Nothing means all environments
        'assertion' =  ought to -Be $true
        
    }
)

Right here, you possibly can outline the environments within the caller script to execute the scripts. For instance, if in case you have an surroundings for every knowledge supply I used for Devolutions RDM, my environments are xml, sqllite, and so forth.

Helper Features

Lastly, we’ve got the helper features script. This script comprises features we dot supply within the BeforeAll block within the caller script. That is the place you possibly can put any features you’ll be reusing. In my instance, I’ve features to arrange knowledge sources and take away all of them.

# Helper perform to take away all entries from the present knowledge supply
# Used for cleanup in take a look at situations
perform Take away-AllEntries {
    strive {
        # Get all entries and their IDs
        # Utilizing ErrorAction SilentlyContinue to deal with case the place no entries exist
        $entries = @(Get-RDMEntry -ErrorAction SilentlyContinue)

        # If no entries discovered, simply return silently
        # No cleanup wanted on this case
        if ($entries.Rely -eq 0) {
            return
        }

        # Delete entries separately
        # Utilizing foreach loop to deal with errors for particular person entries
        foreach ($entry in $entries) {
            strive {
                # Take away entry and refresh to make sure UI is up to date
                Take away-RDMEntry -ID $entry.ID -Refresh -ErrorAction Cease
            } catch [System.Management.Automation.ItemNotFoundException] {
                # Silently ignore if entry is already gone
                # This will occur if entry was deleted by one other course of
                proceed
            }
        }
    } catch {
        # Solely warn about surprising errors
        # Ignore "Connection not discovered" as that is anticipated in some instances
        if ($_.Exception.Message -ne 'Connection not discovered.') {
            Write-Warning "Error throughout cleanup: $_"
        }
    }
}

# Helper perform to take away all knowledge sources and their information
# Used for cleanup in take a look at situations
perform Take away-AllDatasources {
    # Take away any knowledge sources at present loaded in reminiscence
    # This ensures clear state for exams
    Get-RDMDataSource | ForEach-Object { Take away-RDMDataSource -DataSource $_ }

    # Delete any present knowledge supply folders on disk
    # These are recognized by GUIDs within the RDM utility knowledge folder
    Get-ChildItem $env:LOCALAPPDATADevolutionsRemoteDesktopManager | 
        The place-Object { $_.Identify -match '^[0-9a-fA-F-]{36}
}

How one can Construct Your Framework

Does this framework look helpful? Do you assume it’d be useful to your group? If that’s the case, listed here are a couple of ideas and questions to consider.

  1. What number of exams do you anticipate needing? Is your use case sufficient to help this framework as a substitute of simply a few take a look at scripts?
  2. Earlier than writing any PowerShell, describe the way you anticipate defining teams and subgroups. Make them a logical abstraction of no matter parts you might be testing.
  3. Take into consideration environments and the configuration you’d want for every set of exams; create these as contexts within the caller script.
  4. As you construct take a look at definitions, if you end up repeating code, create a perform for it and put it in helper_functions.ps1.

Constructing a scalable Pester testing framework requires cautious planning and group, however the advantages are well worth the effort. By following the construction outlined on this article – with its modular design, reusable parts, and versatile surroundings dealing with – you possibly can create a sturdy testing answer that grows together with your wants. Whether or not you’re testing PowerShell modules, infrastructure configurations, or complicated functions, this framework supplies a stable basis for sustaining code high quality and reliability throughout your group.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments