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.
- By Adam Bertram
- 02/01/2018
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 a 20-year veteran of IT. He's an automation engineer, blogger, consultant, freelance writer, Pluralsight course author and content marketing advisor to multiple technology companies. Adam also founded the popular TechSnips e-learning platform. He mainly focuses on DevOps, system management and automation technologies, as well as various cloud platforms mostly in the Microsoft space. He is a Microsoft Cloud and Datacenter Management MVP who absorbs knowledge from the IT field and explains it in an easy-to-understand fashion. Catch up on Adam's articles at adamtheautomator.com, connect on LinkedIn or follow him on Twitter at @adbertram or the TechSnips Twitter account @techsnips_io.