Ironman Software Forums
Continue the conversion on the Ironman Software forums. Chat with over 1000 users about PowerShell, PowerShell Universal, and PowerShell Pro Tools.
Learn how to write tests against PowerShell Universal with Pester 5.
In this blog, we’ll use InvokeBuild
and Pester
. You’ll need to install these modules to follow along.
Install-Module Pester
Install-Module InvokeBuild
The first step is to download and install the new version of PowerShell Universal that you wish to test against your configuration. You can do so in two different ways. First, you can use Invoke-WebRequest
directly to download the file, or you can use Install-PSUServer
to download the MSI and install the service.
In this example, I’ll use Invoke-WebRequest
since I’ll be downloading the nightly build. The following scripts reads the nightly blob storage, finds the latest build, downloads the ZIP, extracts it and unblocks the files.
[xml]$Xml = (Invoke-RestMethod 'https://imsreleases.blob.core.windows.net/universal-nightly?restype=container&comp=list').Substring(3)
$MaxBlob = $null
foreach($blob in $Xml.EnumerationResults.Blobs.Blob.Where({$_.Url.Contains('win7') -and $_.Url.Contains(".zip")}))
{
if ($null -eq $MaxBlob -or ([int]$blob.Name.Split('/')[0]) -gt ([int]$MaxBlob.Name.Split('/')[0]))
{
$MaxBlob = $blob
}
}
Invoke-WebRequest $MaxBlob.Url -OutFile "$PSScriptRoot\Universal.zip"
Expand-Archive -Path "$PSScriptRoot\Universal.zip" -Destination "$PSScriptRoot\Universal"
Get-ChildItem "$PSScriptRoot\Universal" -Recurse | Unblock-File
You could also update the above to download the current released version.
Invoke-WebRequest "https://imsreleases.blob.core.windows.net/universal/production/2.5.4/Universal.win7-x64.2.5.4.zip" -OutFile "$PSScriptRoot\Universal.zip"
Once we have PowerShell Universal downloaded, we can configure PSU with some pre-built configuration files. In our integration test suite, we have PS1 files that we load into the configuration directory before starting the server.
Our test files having many permutations of PSU configuration. This is a sample of our endpoints.ps1
.
New-PSUEndpoint -Url "/get" -Method 'GET' -Endpoint {
"Hello"
}
New-PSUEndpoint -Url "/get/header" -Method 'GET' -Endpoint {
$Headers["X-MYHEADER"]
}
New-PSUEndpoint -Url "/get/:id" -Method 'GET' -Endpoint {
$Id
}
New-PSUEndpoint -Url "/post" -Method 'POST' -Endpoint {
$Body
}
New-PSUEndpoint -Url "/post/params" -Method 'POST' -Endpoint {
param($Name, $Value)
@{
Name = $Name
Value = $Value
}
}
New-PSUEndpoint -Url "/error" -Endpoint {
throw "Uh oh!"
} -ErrorAction stop
We then take this configuration and deploy it to the configuration directory C:\ProgramData\UniversalAutomation\Repository
.
New-Item C:\ProgramData\UniversalAutomation -ItemType Directory
Copy-Item "$PSScriptRoot\assets\Repository" C:\ProgramData\UniversalAutomation -Recurse
Next, we can get the PSU server up and running and ready for our Pester test suite to run. The following script starts the PSU server, waits for it to become active, logs in and then grants an app token we can use later on within our tests.
Import-Module "$PSScriptRoot\Universal\Universal.psd1"
$Process = Start-Process "$PSScriptRoot\Universal\Universal.Server.exe" -PassThru
# Wait for the PSU server to start
while($true)
{
try
{
Invoke-RestMethod "http://localhost:5000/api/v1/alive"
break;
}
catch
{
}
}
try
{
# Sign in using default forms auth.
Invoke-RestMethod "http://localhost:5000/api/v1/signin" -Method Post -Body (@{
Username = "admin"
Password = "admin"
} | ConvertTo-Json) -SessionVariable Session -ContentType "application/json"
# Grant an app token using the login session
$ENV:TestAppToken = (Invoke-RestMethod "http://localhost:5000/api/v1/apptoken/grant" -Method GET -WebSession $Session).Token
# Connect to the PSU server so we can use cmdlets
Connect-PSUServer -AppToken $ENV:TestAppToken -ComputerName "http://localhost:5000"
# Set the current location and start executing tests
Set-Location $PSScriptRoot
$Results = Invoke-Pester -PassThru
if ($Results.Result -ne 'Passed')
{
throw "Tests failed!"
}
}
finally
{
Stop-Process $Process
}
The next step is to actually run the validation scripts using Pester. This post was done with Pester 5.3.1.
Here is a subset of some of the tests we run for scripts. We take advantage of a data driven test that allows us to run against multiple environments by providing a -ForEach
array to the Describe
block.
All of our tests in this example will run three times; one for each environment. We are also taking advantage of the BeforeAll
block to set the environment before each environment test is run.
Finally, each It
block validates individual functionality.
Describe "Scripts.<_>" -ForEach @('pwsh', 'powershell', 'integrated') {
BeforeAll {
Set-PSUSetting -DefaultEnvironment $_
}
Context "Run" {
It "should have correct variables" {
$Vars = Invoke-PSUScript -Name 'Vars.ps1' -Wait
$Vars.Environment | Should -be $_
$Vars.Script.Name | Should -be "Vars.ps1"
$Vars.Job | Should -not -be $null
$Vars.JobId | Should -not -be $null
$Vars.ScriptId | should -not -be $null
$Vars.Simple | Should -be "123"
}
It "should run a script by name" {
$Job = Invoke-PSUScript -Name 'Script.ps1'
$Job | Should -not -be $null
}
It "should run a script by pipeline" {
{ Get-PSUScript -Name 'Script.ps1' | Invoke-PSUScript } | Should -Throw
}
It "should return a hashtable" {
$Output = Invoke-PSUScript -Name 'Output.ps1' -Wait
$Output.Name | Should -be 'Tutorial'
$Output.Description | Should -be 'Tutorial'
}
It "should pass parameters to script" {
$Output = Invoke-PSUScript -Name 'Params.ps1' -Wait -One 1 -Two 2
$Output.One | Should -be 1
$Output.Two | Should -be 2
}
It "should call script in folder" {
Invoke-PSUScript -Name 'Script2.ps1' -Wait | Should -be 'Hello'
}
}
}
Now that we have the test framework developed and the tests authored, we can wrap them up in a handy InvokeBuild
script. This will make it easier to call individual portions of the script.
Here’s the full example of the script. We have setup three build tasks. One is to clean up after previous test runs. The second is to download and extract the nightly build. The final task runs the test suite.
task Clean {
Remove-Item -Path "C:\ProgramData\PowerShellUniversal" -Force -ErrorAction SilentlyContinue -Recurse
Remove-Item -Path "C:\ProgramData\UniversalAutomation" -Force -ErrorAction SilentlyContinue -Recurse
Remove-Item -Path "$PSScriptRoot\Universal" -Force -ErrorAction SilentlyContinue -Recurse
Remove-Item -Path "$PSScriptRoot\Universal.zip" -Force -ErrorAction SilentlyContinue
}
task DownloadNightly {
[xml]$Xml = (Invoke-RestMethod 'https://imsreleases.blob.core.windows.net/universal-nightly?restype=container&comp=list').Substring(3)
$MaxBlob = $null
foreach($blob in $Xml.EnumerationResults.Blobs.Blob.Where({$_.Url.Contains('win7') -and $_.Url.Contains(".zip")}))
{
if ($null -eq $MaxBlob -or ([int]$blob.Name.Split('/')[0]) -gt ([int]$MaxBlob.Name.Split('/')[0]))
{
$MaxBlob = $blob
}
}
Invoke-WebRequest $MaxBlob.Url -OutFile "$PSScriptRoot\Universal.zip"
Expand-Archive -Path "$PSScriptRoot\Universal.zip" -Destination "$PSScriptRoot\Universal"
Get-ChildItem "$PSScriptRoot\Universal" -Recurse | Unblock-File
}
task RunTests {
New-Item C:\ProgramData\UniversalAutomation -ItemType Directory
Copy-Item "$PSScriptRoot\assets\Repository" C:\ProgramData\UniversalAutomation -Recurse
Import-Module "$PSScriptRoot\Universal\Universal.psd1"
$Process = Start-Process "$PSScriptRoot\Universal\Universal.Server.exe" -PassThru
while($true)
{
try
{
Invoke-RestMethod "http://localhost:5000/api/v1/alive"
break;
}
catch
{
}
}
try
{
Invoke-RestMethod "http://localhost:5000/api/v1/signin" -Method Post -Body (@{
Username = "admin"
Password = "admin"
} | ConvertTo-Json) -SessionVariable Session -ContentType "application/json"
$ENV:TestAppToken = (Invoke-RestMethod "http://localhost:5000/api/v1/apptoken/grant" -Method GET -WebSession $Session).Token
Connect-PSUServer -AppToken $ENV:TestAppToken -ComputerName "http://localhost:5000"
Set-Location $PSScriptRoot
$Results = Invoke-Pester -PassThru
if ($Results.Result -ne 'Passed')
{
throw "Tests failed!"
}
}
finally
{
Stop-Process $Process
}
}
task CleanAndRun Clean, RunTests
task . Clean, DownloadNightly, RunTests
In our environment, we use GitHub Actions for CI and CD pipelines. We have a self-hosted agent designated for running integration tests. From within the Universal repository, we’ve setup a GitHub Actions workflow yml
file to run our tests on our custom agent. The workflow runs each morning and can be triggered manually.
We are calling Invoke-Build
to run our integration tests. If a test fails, an exception is thrown and the workflow will also fail.
name: Integration Tests
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
jobs:
build:
name: Build
runs-on: self-hosted
steps:
- uses: actions/checkout@v1
- name: Run Integration Test
run: Invoke-Build -File .\test\integration-test.ps1
shell: pwsh
The resulting output of the Action provides information about which tests are successful and which have failed. Here’s the result of one of our test suites.
Find this useful? Please consider sharing this article. Have a question about PowerShell? Contact us and we'll write a post about it.
Continue the conversion on the Ironman Software forums. Chat with over 1000 users about PowerShell, PowerShell Universal, and PowerShell Pro Tools.
Receive once-a-month updates about Ironman Software. You'll learn about our product updates and blogs related to PowerShell.