Hovedmenu
Devolutions-blog

Nyheder, opdateringer og indsigter fra Devolutions.

Pester turtle and TDD cycle of fail, pass, and refactor for PowerShell Universal.

Brug Pester 5 til at teste instanser af PowerShell Universal

Denne gennemgang viser, hvordan du rejser en isoleret PowerShell Universal-instans, klargør den med konfiguration, skriver datadrevne Pester 5-tests mod scripts og endpoints og derefter pakker suiten ind i InvokeBuild og GitHub Actions.

Lær, hvordan du skriver tests mod PowerShell Universal med Pester 5.

Installer forudsætninger

I dette indlæg bruger vi InvokeBuild og Pester. Du skal installere disse moduler for at følge med.

Install-Module Pester
Install-Module InvokeBuild

Download PowerShell Universal

Første skridt er at downloade og installere den version af PowerShell Universal, du vil teste din konfiguration imod. Det kan du gøre på to måder. Du kan bruge Invoke-WebRequest direkte til at downloade ZIP-filen, eller du kan bruge Install-PSUServer til at downloade MSI-filen og installere tjenesten.

I dette eksempel bruger jeg Invoke-WebRequest, fordi jeg downloader nightly-buildet. Scriptet herunder læser nightly-bloblageret, finder det seneste build, downloader ZIP-filen, pakker den ud og fjerner blokeringen af filerne.

[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

Du kan også tilpasse scriptet ovenfor, så det downloader en udgivet 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"

Konfigurer PowerShell Universal

Når PowerShell Universal er downloadet, kan vi konfigurere PSU med nogle forhåndsbyggede konfigurationsfiler. I vores integrationstestsuite har vi .ps1-filer, som vi indlæser i konfigurationsmappen, før serveren startes.

PowerShell Universal test repository folder with Scripts, dashboards, and published folders.
Layout af testrepositoriet til PowerShell Universal

Vores testfiler har mange varianter af PSU-konfiguration. Her er et udsnit af vores 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

Derefter tager vi denne konfiguration og udruller den til konfigurationsmappen C:\ProgramData\UniversalAutomation\Repository.

New-Item C:\ProgramData\UniversalAutomation -ItemType Directory
Copy-Item "$PSScriptRoot\assets\Repository" C:\ProgramData\UniversalAutomation -Recurse

Klargør en Pester-test

Dernæst kan vi få PSU-serveren op at køre og klar til vores Pester-testsuite. Scriptet herunder starter PSU-serveren, venter på, at den bliver aktiv, logger ind og udsteder derefter et app-token, som vi kan bruge senere i testene.

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
}

Skriv en Pester-test

Næste skridt er at køre valideringsscripts med Pester. Dette indlæg er skrevet mod Pester 5.3.1.

Her er et udsnit af nogle af de tests, vi kører for scripts. Vi udnytter en datadrevet test, der lader os køre mod flere miljøer ved at give et -ForEach-array til Describe-blokken.

Alle vores tests i dette eksempel kører tre gange, én gang for hvert miljø. Vi bruger også BeforeAll-blokken til at sætte miljøet, før testene for hvert miljø kører.

Til sidst validerer hver It-blok en enkelt funktion med cmdlets som Invoke-PSUScript.

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'
        }
    }
}

Pak det ind i et InvokeBuild-script

Nu hvor vi har testrammen og testene, kan vi pakke dem ind i et InvokeBuild-script. Det gør det nemmere at kalde enkelte dele af workflowet.

Her er det fulde eksempel. Vi har sat tre build-opgaver op. Én rydder op efter tidligere testkørsler. Den anden downloader og pakker nightly-buildet ud. Den sidste opgave kører testsuiten.

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

Kør Pester-tests i GitHub Actions

I vores miljø bruger vi GitHub Actions til CI- og CD-pipelines. Vi har en self-hosted agent, der er afsat til integrationstests. Fra Universal-repositoriet har vi sat en GitHub Actions-workflow-YAML-fil op, så testene kører på vores egen agent. Workflowet kører hver morgen og kan udløses manuelt.

Vi kalder Invoke-Build for at køre integrationstestene. Hvis en test fejler, kastes en undtagelse, og workflowet fejler også.

name: Integration Tests
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

Action-outputtet viser, hvilke tests der lykkedes, og hvilke der fejlede. Her er resultatet af en af vores testsuites.

Pester test run output showing 68 passed tests and one failed dashboard test.
Pester-resultater fra en integrationskørsel i GitHub Actions

Klar til at bygge? Download PowerShell Universal.