MAIN MENU
Devolutions Blog

Ankündigungen, Aktualisierungen und Einsichten von Devolutions

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

PowerShell Universal-Instanzen mit Pester 5 testen

Diese Anleitung zeigt, wie Sie eine isolierte PowerShell Universal-Instanz starten, sie mit Konfiguration füllen, datengesteuerte Pester-5-Tests gegen Skripte und Endpunkte schreiben und die Suite in InvokeBuild und GitHub Actions verpacken.

Lernen Sie, Tests gegen PowerShell Universal mit Pester 5 zu schreiben.

Voraussetzungen installieren

In diesem Beitrag nutzen wir InvokeBuild und Pester. Sie müssen diese Module installieren, um mitzumachen.

Install-Module Pester
Install-Module InvokeBuild

PowerShell Universal herunterladen

Der erste Schritt besteht darin, die Version von PowerShell Universal herunterzuladen und zu installieren, gegen die Sie Ihre Konfiguration testen möchten. Das geht auf zwei Wegen. Sie können Invoke-WebRequest direkt nutzen, um das ZIP herunterzuladen, oder Install-PSUServer, um das MSI herunterzuladen und den Dienst zu installieren.

In diesem Beispiel verwende ich Invoke-WebRequest, weil ich den Nightly-Build herunterlade. Das folgende Skript liest den Nightly-Blob-Speicher, findet den neuesten Build, lädt das ZIP herunter, entpackt es und hebt die Dateisperre auf.

[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

Sie können das Skript oben auch anpassen, um eine veröffentlichte Version herunterzuladen.

Invoke-WebRequest "https://imsreleases.blob.core.windows.net/universal/production/2.5.4/Universal.win7-x64.2.5.4.zip" -OutFile "$PSScriptRoot\Universal.zip"

PowerShell Universal konfigurieren

Sobald PowerShell Universal heruntergeladen ist, können wir PSU mit vorbereiteten Konfigurationsdateien einrichten. In unserer Integrationstest-Suite haben wir .ps1-Dateien, die wir vor dem Start des Servers in das Konfigurationsverzeichnis laden.

PowerShell Universal test repository folder with Scripts, dashboards, and published folders.
Test-Repository-Struktur für PowerShell Universal

Unsere Testdateien enthalten viele Varianten der PSU-Konfiguration. Dies ist ein Ausschnitt unserer 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

Anschließend übernehmen wir diese Konfiguration und stellen sie im Verzeichnis C:\ProgramData\UniversalAutomation\Repository bereit.

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

Vorbereitung für einen Pester-Test

Als Nächstes starten wir den PSU-Server und machen ihn bereit für unsere Pester-Testsuite. Das folgende Skript startet den PSU-Server, wartet, bis er aktiv ist, meldet sich an und stellt dann ein App-Token aus, das wir später in den Tests verwenden können.

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
}

Einen Pester-Test schreiben

Der nächste Schritt besteht darin, die Validierungsskripte mit Pester auszuführen. Dieser Beitrag wurde gegen Pester 5.3.1 geschrieben.

Hier ist eine Teilmenge der Tests, die wir für Skripte ausführen. Wir nutzen einen datengesteuerten Test, der uns mit einem -ForEach-Array am Describe-Block gegen mehrere Umgebungen laufen lässt.

Alle Tests in diesem Beispiel laufen dreimal, einmal pro Umgebung. Wir nutzen außerdem den BeforeAll-Block, um die Umgebung festzulegen, bevor die Tests der jeweiligen Umgebung laufen.

Schließlich prüft jeder It-Block einzelne Funktionen mit Cmdlets wie 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'
        }
    }
}

In ein InvokeBuild-Skript verpacken

Nachdem Testframework und Tests stehen, können wir sie in ein InvokeBuild-Skript packen. So lassen sich einzelne Teile des Ablaufs leichter aufrufen.

Hier das vollständige Beispiel. Wir haben drei Build-Aufgaben eingerichtet. Eine räumt nach früheren Testläufen auf. Die zweite lädt den Nightly-Build herunter und entpackt ihn. Die letzte Aufgabe führt die Testsuite aus.

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

Pester-Tests in GitHub Actions ausführen

In unserer Umgebung nutzen wir GitHub Actions für CI- und CD-Pipelines. Wir haben einen selbst gehosteten Agenten für Integrationstests. Im Universal-Repository haben wir eine GitHub-Actions-Workflow-YAML eingerichtet, um unsere Tests auf diesem Agenten auszuführen. Der Workflow läuft jeden Morgen und kann manuell ausgelöst werden.

Wir rufen Invoke-Build auf, um die Integrationstests auszuführen. Schlägt ein Test fehl, wird eine Ausnahme ausgelöst und der Workflow schlägt ebenfalls fehl.

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

Die Ausgabe der Action zeigt, welche Tests erfolgreich waren und welche fehlgeschlagen sind. Hier das Ergebnis einer unserer Testsuites.

Pester test run output showing 68 passed tests and one failed dashboard test.
Pester-Ergebnisse eines GitHub-Actions-Integrationslaufs

Bereit zum Bauen? PowerShell Universal herunterladen.