PowerShell How-To

Ensuring a Clean PowerShell Session for Your Pester Tests

It's important to start tests with a clean session, especially when writing unit tests and creating mocks. Here's how to run Pester tests in a completely new PowerShell process.

Writing unit tests in Pester is a topic that is sometimes unpredictable. There are numerous occasions where environmental factors like loaded .NET assemblies, imported modules and declared variables that are available in the current PowerShell session conflict with your tests.

Especially when writing unit tests and creating mocks, it's important to start your tests with a clean session. To ensure that we only run tests in a pristine session, let's go over how to run Pester tests in a completely new PowerShell process.

Since Invoke-Pester doesn't have a NewRunspace parameter, I've created a simple wrapper called Start-UnitTest that I use. The Start-UnitTest command takes a Path parameter, which is the same parameter as Invoke-Pester that designates the test script. Inside of Start-UnitTest, I have a few helper functions I've built called InvokePowerShellCommand and ConvertHashTableParametersToString. All of the functions are shown below:

function InvokePowerShellCommand
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Command
    )
    $Command | powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -Command -
}

function ConvertTo-String
{
    [OutputType([string])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [hashtable]$Hashtable
    )
    $arr = $Hashtable.GetEnumerator().foreach({
        if ($_.Value -is 'hashtable') {
            "'$($_.Key)' = $(ConvertTo-String -HashTable $_.Value)"
        } else {
            "'$($_.Key)' = '$($_.Value)'"
        }
    })
    '@{{ {0} }}' -f ($arr -join ';')
}

function ConvertHashTableParametersToString
{
    [OutputType([string])]
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [hashtable]$Parameters
    )
    $Parameters.GetEnumerator().foreach({
        $paramName = $_.Key
        if ($_.Value -is 'hashtable') {
            $paramValue = ConvertTo-String -HashTable $_.Value
        } else {
            $paramValue = '"{0}"' -f (@($_.Value) -join '","')
        }

        '-{0} {1}' -f $paramName,$paramValue
    })
}

function Start-UnitTest
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )

    $invPesterParams = @{
        Path = $Path
    }

    ## Can't splat here because we're passing to another powershell.exe process
    $invPesterParamString = ConvertHashTableParametersToString -Parameters $invPesterParams

    InvokePowerShellCommand -Command "Invoke-Pester $invPesterParamString"

}

When Start-UnitTest is called, it will build a hash table of parameters to pass to Invoke-Pester. If we were simply calling Invoke-Pester without worrying about another runspace, we'd just pass that hashtable directly to Invoke-Pester.

$invPesterParams = @{ Path = 'C:\Test.ps1' }
Invoke-Pester @invPesterParams

However, since we're invoking a brand-new PowerShell process, we must convert this hash table to a string that we can then pass to the separate powershell.exe process using the Command parameter. You can see from the functions above that I've created the InvokePowerShellCommand function to do just that.

Let's say we have a test script at C:\Test.ps1. Running this test script in the same PowerShell process looks like this:

PS C:\> invoke-pester -path C:\Test.ps1
Executing all tests in 'C:\Test.ps1'

Executing script C:\Test.ps1

  Describing test stuff here
    [+] does that thing I like 586ms
Tests completed in 586ms
Tests Passed: 1, Failed: 0, Skipped: 0, Pending: 0, Inconclusive: 0

Let's now run this with our Start-UnitTest wrapper. I'll put all of my functions above into a file called C:\TestHelpers.ps1 and dot-source them so they're available in my session.

. C:\TestHelpers.ps1

Next, I'll just run Start-UnitTest using the same Path parameter as if I was just using Invoke-Pester.

PS C:\> Start-UnitTest -Path C:\Test.ps1
Executing all tests in 'C:\Test.ps1'

Executing script C:\Test.ps1

  Describing test stuff here
    [+] does that thing I like 722ms
Tests completed in 722ms
Tests Passed: 1, Failed: 0, Skipped: 0, Pending: 0, Inconclusive: 0

Do you see a difference? I don't! The only difference you might notice is a slight delay while the new PowerShell process is brought up.

Building a wrapper like this allows you to not worry about when to invoke tests in a new runspace. You'll eventually just get to the point where Start-UnitTest will be your de facto test runner and you will forget all about Invoke-Pester itself!

About the Author

Adam Bertram is an independent consultant, technical writer, trainer and presenter. Adam specializes in consulting and evangelizing all things IT automation mainly focused around Windows PowerShell. Adam is a Microsoft Windows PowerShell MVP, 2015 powershell.org PowerShell hero and has numerous Microsoft IT pro certifications. He is a writer, trainer and presenter and authors IT pro course content for Pluralsight. He is also a regular contributor to numerous print and online publications and presents at various user groups and conferences. You can find Adam at adamtheautomator.com or on Twitter at @adbertram.

comments powered by Disqus

SharePoint Watch

Sign up for our newsletter.

I agree to this site's Privacy Policy.