MAIN MENU
Blog de Devolutions

Anuncios, actualizaciones y análisis de Devolutions.

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

Probar instancias de PowerShell Universal con Pester 5

Esta guía muestra cómo levantar una instancia aislada de PowerShell Universal, cargarla con configuración, escribir pruebas Pester 5 basadas en datos contra scripts y endpoints, y empaquetar la suite en InvokeBuild y GitHub Actions.

Aprenda a escribir pruebas contra PowerShell Universal con Pester 5.

Instalar requisitos previos

En esta entrada usaremos InvokeBuild y Pester. Deberá instalar estos módulos para seguir el ejemplo.

Install-Module Pester
Install-Module InvokeBuild

Descargar PowerShell Universal

El primer paso es descargar e instalar la versión de PowerShell Universal contra la que desea probar su configuración. Puede hacerlo de dos formas. Puede usar Invoke-WebRequest directamente para descargar el ZIP, o usar Install-PSUServer para descargar el MSI e instalar el servicio.

En este ejemplo usaré Invoke-WebRequest porque descargaré la compilación nightly. El script siguiente lee el almacenamiento blob nightly, encuentra la última compilación, descarga el ZIP, lo extrae y desbloquea los archivos.

[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

También puede adaptar el script anterior para descargar una versión publicada.

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

Configurar PowerShell Universal

Una vez descargado PowerShell Universal, podemos configurar PSU con archivos de configuración ya preparados. En nuestra suite de pruebas de integración tenemos archivos .ps1 que cargamos en el directorio de configuración antes de arrancar el servidor.

PowerShell Universal test repository folder with Scripts, dashboards, and published folders.
Estructura del repositorio de pruebas para PowerShell Universal

Nuestros archivos de prueba cubren muchas permutaciones de configuración de PSU. Este es un fragmento de nuestro 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

A continuación tomamos esta configuración y la desplegamos en el directorio C:\ProgramData\UniversalAutomation\Repository.

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

Preparar un test de Pester

A continuación podemos arrancar el servidor PSU y dejarlo listo para la suite de Pester. El script siguiente inicia el servidor PSU, espera a que esté activo, inicia sesión y concede un app token que usaremos más adelante en las pruebas.

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
}

Escribir un test de Pester

El siguiente paso es ejecutar los scripts de validación con Pester. Esta entrada se escribió contra Pester 5.3.1.

Aquí hay un subconjunto de las pruebas que ejecutamos para scripts. Aprovechamos una prueba basada en datos que nos permite ejecutar contra varios entornos al pasar una matriz -ForEach al bloque Describe.

Todas las pruebas de este ejemplo se ejecutarán tres veces, una por cada entorno. También usamos el bloque BeforeAll para establecer el entorno antes de que se ejecuten las pruebas de cada entorno.

Por último, cada bloque It valida una funcionalidad concreta con cmdlets como 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'
        }
    }
}

Empaquetar en un script InvokeBuild

Con el marco de pruebas y las pruebas listas, podemos envolverlos en un script InvokeBuild. Así es más fácil llamar a cada parte del flujo.

Este es el ejemplo completo. Hemos definido tres tareas de build. Una limpia las ejecuciones de pruebas anteriores. La segunda descarga y extrae la compilación nightly. La última ejecuta la suite de pruebas.

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

Ejecutar pruebas Pester en GitHub Actions

En nuestro entorno usamos GitHub Actions para las canalizaciones de CI y CD. Tenemos un agente autohospedado dedicado a las pruebas de integración. En el repositorio Universal hemos configurado un archivo YAML de flujo de GitHub Actions para ejecutar las pruebas en ese agente. El flujo se ejecuta cada mañana y se puede desencadenar de forma manual.

Llamamos a Invoke-Build para ejecutar las pruebas de integración. Si una prueba falla, se lanza una excepción y el flujo también falla.

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 salida de la Action indica qué pruebas se superaron y cuáles fallaron. Este es el resultado de una de nuestras suites.

Pester test run output showing 68 passed tests and one failed dashboard test.
Resultados de Pester de una ejecución de integración en GitHub Actions

¿Listo para crear? Descargue PowerShell Universal.