MAIN MENU
Le blogue Devolutions

Annonces, mises à jour et analyses de Devolutions

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

Tester des instances PowerShell Universal avec Pester 5

Ce tutoriel montre comment démarrer une instance isolée de PowerShell Universal, y charger une configuration, écrire des tests Pester 5 data-driven contre des scripts et des points de terminaison, puis encapsuler la suite dans InvokeBuild et GitHub Actions.

Apprenez à écrire des tests contre PowerShell Universal avec Pester 5.

Installation des prérequis

Dans cet article, nous utiliserons InvokeBuild et Pester. Vous devrez installer ces modules pour suivre.

Install-Module Pester
Install-Module InvokeBuild

Télécharger PowerShell Universal

La première étape consiste à télécharger et installer la version de PowerShell Universal contre laquelle vous voulez tester votre configuration. Vous pouvez le faire de deux façons. Vous pouvez utiliser Invoke-WebRequest directement pour télécharger le ZIP, ou utiliser Install-PSUServer pour télécharger le MSI et installer le service.

Dans cet exemple, j’utiliserai Invoke-WebRequest puisque je télécharge la build nightly. Le script suivant lit le stockage blob nightly, trouve la dernière build, télécharge le ZIP, l’extrait et débloque les fichiers.

[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

Vous pourriez aussi adapter le script ci-dessus pour télécharger une version publiée.

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

Configurer PowerShell Universal

Une fois PowerShell Universal téléchargé, nous pouvons configurer PSU avec des fichiers de configuration déjà préparés. Dans notre suite de tests d’intégration, nous avons des fichiers .ps1 que nous chargeons dans le répertoire de configuration avant de démarrer le serveur.

PowerShell Universal test repository folder with Scripts, dashboards, and published folders.
Structure du dépôt de tests pour PowerShell Universal

Nos fichiers de test couvrent de nombreuses permutations de configuration PSU. Voici un extrait de notre 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

Nous prenons ensuite cette configuration et la déployons dans le répertoire C:\ProgramData\UniversalAutomation\Repository.

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

Préparer un test Pester

Ensuite, nous pouvons démarrer le serveur PSU et le préparer pour notre suite de tests Pester. Le script suivant démarre le serveur PSU, attend qu’il soit actif, ouvre une session, puis accorde un app token que nous pourrons utiliser plus tard dans les 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
}

Écrire un test Pester

L’étape suivante consiste à exécuter les scripts de validation avec Pester. Cet article a été écrit avec Pester 5.3.1.

Voici un sous-ensemble des tests que nous exécutons pour les scripts. Nous tirons parti d’un test data-driven qui nous permet de cibler plusieurs environnements en fournissant un tableau -ForEach au bloc Describe.

Tous les tests de cet exemple s’exécuteront trois fois, une fois pour chaque environnement. Nous utilisons aussi le bloc BeforeAll pour définir l’environnement avant les tests de chaque environnement.

Enfin, chaque bloc It valide une fonctionnalité, avec des applets de commande comme 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'
        }
    }
}

Empaqueter dans un script InvokeBuild

Maintenant que le cadre de test et les tests sont en place, nous pouvons les encapsuler dans un script InvokeBuild. Cela facilite l’appel de chaque portion du flux.

Voici l’exemple complet. Nous avons défini trois tâches de build. L’une nettoie les exécutions de tests précédentes. La deuxième télécharge et extrait la build nightly. La dernière exécute la suite de tests.

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

Exécuter des tests Pester dans GitHub Actions

Dans notre environnement, nous utilisons GitHub Actions pour les pipelines CI et CD. Nous avons un agent auto-hébergé dédié aux tests d’intégration. Dans le dépôt Universal, nous avons configuré un fichier YAML de workflow GitHub Actions pour exécuter nos tests sur cet agent. Le workflow s’exécute chaque matin et peut être déclenché manuellement.

Nous appelons Invoke-Build pour lancer les tests d’intégration. Si un test échoue, une exception est levée et le workflow échoue aussi.

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

La sortie de l’Action indique quels tests ont réussi et lesquels ont échoué. Voici le résultat d’une de nos suites de tests.

Pester test run output showing 68 passed tests and one failed dashboard test.
Résultats Pester d’une exécution d’intégration GitHub Actions

Prêt à construire? Téléchargez PowerShell Universal.